mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-19 06:50:24 +01:00
Compare commits
12 Commits
v0.70.0
...
fix/instal
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abea45f255 | ||
|
|
d1e7cc128f | ||
|
|
ffd72cf406 | ||
|
|
6dfea14219 | ||
|
|
f2be856f63 | ||
|
|
f04589a0b2 | ||
|
|
1378590429 | ||
|
|
88084af4d4 | ||
|
|
d0eefa0cf2 | ||
|
|
cc9eb32c50 | ||
|
|
c0d8f8de3a | ||
|
|
860fa4a995 |
19
.github/workflows/pr_verify_linked_issue.yml
vendored
19
.github/workflows/pr_verify_linked_issue.yml
vendored
@@ -1,19 +0,0 @@
|
||||
# This workflow will inspect a pull request to ensure there is a linked issue or a
|
||||
# valid issue is mentioned in the body. If neither is present it fails the check and adds
|
||||
# a comment alerting users of this missing requirement.
|
||||
name: VerifyIssue
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [edited, opened]
|
||||
check_run:
|
||||
|
||||
jobs:
|
||||
verify_linked_issue:
|
||||
runs-on: ubuntu-latest
|
||||
name: Ensure Pull Request has a linked issue.
|
||||
steps:
|
||||
- name: Verify Linked Issue
|
||||
uses: srikanthccv/verify-linked-issue-action@v0.71
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -77,4 +77,19 @@ apiserver:
|
||||
- /api/v3/logs/livetail
|
||||
logging:
|
||||
excluded_routes:
|
||||
- /api/v1/health
|
||||
- /api/v1/health
|
||||
|
||||
|
||||
##################### TelemetryStore #####################
|
||||
telemetrystore:
|
||||
# specifies the telemetrystore provider to use.
|
||||
provider: clickhouse
|
||||
clickhouse:
|
||||
# The DSN to use for ClickHouse.
|
||||
dsn: http://localhost:9000
|
||||
# Maximum number of idle connections in the connection pool.
|
||||
max_idle_conns: 50
|
||||
# Maximum number of open connections to the database.
|
||||
max_open_conns: 100
|
||||
# Maximum time to wait for a connection to be established.
|
||||
dial_timeout: 5s
|
||||
@@ -309,7 +309,7 @@ request_sudo() {
|
||||
echo -e "Got it! Thanks!! 🙏\n"
|
||||
echo -e "Okay! We will bring up the SigNoz cluster from here 🚀\n"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
echo ""
|
||||
@@ -317,7 +317,7 @@ echo -e "👋 Thank you for trying out SigNoz! "
|
||||
echo ""
|
||||
|
||||
sudo_cmd=""
|
||||
docker_compose_cmd=""
|
||||
docker_compose_cmd="docker compose"
|
||||
|
||||
# Check sudo permissions
|
||||
if (( $EUID != 0 )); then
|
||||
@@ -325,7 +325,13 @@ if (( $EUID != 0 )); then
|
||||
echo " In case of any failure or prompt, please consider running the script with sudo privileges."
|
||||
echo ""
|
||||
else
|
||||
sudo_cmd="sudo"
|
||||
if hash sudo 2>/dev/null; then
|
||||
sudo_cmd="sudo"
|
||||
else
|
||||
echo "🟡 Running installer with root permissions but sudo command not found, running without sudo"
|
||||
echo " In case of any failure or prompt, please consider installing sudo and running the script again."
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
# Checking OS and assigning package manager
|
||||
@@ -466,11 +472,10 @@ if ! is_command_present docker; then
|
||||
fi
|
||||
|
||||
if has_docker_compose_plugin; then
|
||||
echo "docker compose plugin is present, using it"
|
||||
docker_compose_cmd="docker compose"
|
||||
# Install docker-compose
|
||||
echo "docker compose plugin found, using it"
|
||||
else
|
||||
docker_compose_cmd="docker-compose"
|
||||
echo "docker compose plugin not found, using docker-compose instead"
|
||||
if ! is_command_present docker-compose; then
|
||||
request_sudo
|
||||
install_docker_compose
|
||||
|
||||
@@ -26,9 +26,6 @@ type APIHandlerOptions struct {
|
||||
DataConnector interfaces.DataConnector
|
||||
SkipConfig *basemodel.SkipConfig
|
||||
PreferSpanMetrics bool
|
||||
MaxIdleConns int
|
||||
MaxOpenConns int
|
||||
DialTimeout time.Duration
|
||||
AppDao dao.ModelDao
|
||||
RulesManager *rules.Manager
|
||||
UsageManager *usage.Manager
|
||||
@@ -57,9 +54,6 @@ func NewAPIHandler(opts APIHandlerOptions) (*APIHandler, error) {
|
||||
Reader: opts.DataConnector,
|
||||
SkipConfig: opts.SkipConfig,
|
||||
PreferSpanMetrics: opts.PreferSpanMetrics,
|
||||
MaxIdleConns: opts.MaxIdleConns,
|
||||
MaxOpenConns: opts.MaxOpenConns,
|
||||
DialTimeout: opts.DialTimeout,
|
||||
AppDao: opts.AppDao,
|
||||
RuleManager: opts.RulesManager,
|
||||
FeatureFlags: opts.FeatureFlags,
|
||||
@@ -117,13 +111,6 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew
|
||||
// note: add ee override methods first
|
||||
|
||||
// routes available only in ee version
|
||||
router.HandleFunc("/api/v1/licenses",
|
||||
am.AdminAccess(ah.listLicenses)).
|
||||
Methods(http.MethodGet)
|
||||
|
||||
router.HandleFunc("/api/v1/licenses",
|
||||
am.AdminAccess(ah.applyLicense)).
|
||||
Methods(http.MethodPost)
|
||||
|
||||
router.HandleFunc("/api/v1/featureFlags",
|
||||
am.OpenAccess(ah.getFeatureFlags)).
|
||||
@@ -178,11 +165,6 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew
|
||||
router.HandleFunc("/api/v1/dashboards/{uuid}/lock", am.EditAccess(ah.lockDashboard)).Methods(http.MethodPut)
|
||||
router.HandleFunc("/api/v1/dashboards/{uuid}/unlock", am.EditAccess(ah.unlockDashboard)).Methods(http.MethodPut)
|
||||
|
||||
// v2
|
||||
router.HandleFunc("/api/v2/licenses",
|
||||
am.ViewAccess(ah.listLicensesV2)).
|
||||
Methods(http.MethodGet)
|
||||
|
||||
// v3
|
||||
router.HandleFunc("/api/v3/licenses", am.ViewAccess(ah.listLicensesV3)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.applyLicenseV3)).Methods(http.MethodPost)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -64,55 +63,8 @@ type ApplyLicenseRequest struct {
|
||||
LicenseKey string `json:"key"`
|
||||
}
|
||||
|
||||
type ListLicenseResponse map[string]interface{}
|
||||
|
||||
func convertLicenseV3ToListLicenseResponse(licensesV3 []*model.LicenseV3) []ListLicenseResponse {
|
||||
listLicenses := []ListLicenseResponse{}
|
||||
|
||||
for _, license := range licensesV3 {
|
||||
listLicenses = append(listLicenses, license.Data)
|
||||
}
|
||||
return listLicenses
|
||||
}
|
||||
|
||||
func (ah *APIHandler) listLicenses(w http.ResponseWriter, r *http.Request) {
|
||||
licenses, apiError := ah.LM().GetLicenses(context.Background())
|
||||
if apiError != nil {
|
||||
RespondError(w, apiError, nil)
|
||||
}
|
||||
ah.Respond(w, licenses)
|
||||
}
|
||||
|
||||
func (ah *APIHandler) applyLicense(w http.ResponseWriter, r *http.Request) {
|
||||
var l model.License
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&l); err != nil {
|
||||
RespondError(w, model.BadRequest(err), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if l.Key == "" {
|
||||
RespondError(w, model.BadRequest(fmt.Errorf("license key is required")), nil)
|
||||
return
|
||||
}
|
||||
license, apiError := ah.LM().ActivateV3(r.Context(), l.Key)
|
||||
if apiError != nil {
|
||||
RespondError(w, apiError, nil)
|
||||
return
|
||||
}
|
||||
|
||||
ah.Respond(w, license)
|
||||
}
|
||||
|
||||
func (ah *APIHandler) listLicensesV3(w http.ResponseWriter, r *http.Request) {
|
||||
licenses, apiError := ah.LM().GetLicensesV3(r.Context())
|
||||
|
||||
if apiError != nil {
|
||||
RespondError(w, apiError, nil)
|
||||
return
|
||||
}
|
||||
|
||||
ah.Respond(w, convertLicenseV3ToListLicenseResponse(licenses))
|
||||
ah.listLicensesV2(w, r)
|
||||
}
|
||||
|
||||
func (ah *APIHandler) getActiveLicenseV3(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -121,6 +73,7 @@ func (ah *APIHandler) getActiveLicenseV3(w http.ResponseWriter, r *http.Request)
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// return 404 not found if there is no active license
|
||||
if activeLicense == nil {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("no active license found")}, nil)
|
||||
|
||||
@@ -20,22 +20,20 @@ type ClickhouseReader struct {
|
||||
|
||||
func NewDataConnector(
|
||||
localDB *sqlx.DB,
|
||||
ch clickhouse.Conn,
|
||||
promConfigPath string,
|
||||
lm interfaces.FeatureLookup,
|
||||
maxIdleConns int,
|
||||
maxOpenConns int,
|
||||
dialTimeout time.Duration,
|
||||
cluster string,
|
||||
useLogsNewSchema bool,
|
||||
useTraceNewSchema bool,
|
||||
fluxIntervalForTraceDetail time.Duration,
|
||||
cache cache.Cache,
|
||||
) *ClickhouseReader {
|
||||
ch := basechr.NewReader(localDB, promConfigPath, lm, maxIdleConns, maxOpenConns, dialTimeout, cluster, useLogsNewSchema, useTraceNewSchema, fluxIntervalForTraceDetail, cache)
|
||||
chReader := basechr.NewReader(localDB, ch, promConfigPath, lm, cluster, useLogsNewSchema, useTraceNewSchema, fluxIntervalForTraceDetail, cache)
|
||||
return &ClickhouseReader{
|
||||
conn: ch.GetConn(),
|
||||
conn: ch,
|
||||
appdb: localDB,
|
||||
ClickHouseReader: ch,
|
||||
ClickHouseReader: chReader,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,9 +74,6 @@ type ServerOptions struct {
|
||||
DisableRules bool
|
||||
RuleRepoURL string
|
||||
PreferSpanMetrics bool
|
||||
MaxIdleConns int
|
||||
MaxOpenConns int
|
||||
DialTimeout time.Duration
|
||||
CacheConfigPath string
|
||||
FluxInterval string
|
||||
FluxIntervalForTraceDetail string
|
||||
@@ -157,11 +154,9 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
zap.L().Info("Using ClickHouse as datastore ...")
|
||||
qb := db.NewDataConnector(
|
||||
serverOptions.SigNoz.SQLStore.SQLxDB(),
|
||||
serverOptions.SigNoz.TelemetryStore.ClickHouseDB(),
|
||||
serverOptions.PromConfigPath,
|
||||
lm,
|
||||
serverOptions.MaxIdleConns,
|
||||
serverOptions.MaxOpenConns,
|
||||
serverOptions.DialTimeout,
|
||||
serverOptions.Cluster,
|
||||
serverOptions.UseLogsNewSchema,
|
||||
serverOptions.UseTraceNewSchema,
|
||||
@@ -245,7 +240,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
}
|
||||
|
||||
// start the usagemanager
|
||||
usageManager, err := usage.New(modelDao, lm.GetRepo(), reader.GetConn())
|
||||
usageManager, err := usage.New(modelDao, lm.GetRepo(), serverOptions.SigNoz.TelemetryStore.ClickHouseDB(), serverOptions.Config.TelemetryStore.ClickHouse.DSN)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -266,9 +261,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
DataConnector: reader,
|
||||
SkipConfig: skipConfig,
|
||||
PreferSpanMetrics: serverOptions.PreferSpanMetrics,
|
||||
MaxIdleConns: serverOptions.MaxIdleConns,
|
||||
MaxOpenConns: serverOptions.MaxOpenConns,
|
||||
DialTimeout: serverOptions.DialTimeout,
|
||||
AppDao: modelDao,
|
||||
RulesManager: rm,
|
||||
UsageManager: usageManager,
|
||||
|
||||
@@ -32,19 +32,6 @@ func (r *Repo) InitDB(inputDB *sqlx.DB) error {
|
||||
return sqlite.InitDB(inputDB)
|
||||
}
|
||||
|
||||
func (r *Repo) GetLicenses(ctx context.Context) ([]model.License, error) {
|
||||
licenses := []model.License{}
|
||||
|
||||
query := "SELECT key, activationId, planDetails, validationMessage FROM licenses"
|
||||
|
||||
err := r.db.Select(&licenses, query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get licenses from db: %v", err)
|
||||
}
|
||||
|
||||
return licenses, nil
|
||||
}
|
||||
|
||||
func (r *Repo) GetLicensesV3(ctx context.Context) ([]*model.LicenseV3, error) {
|
||||
licensesData := []model.LicenseDB{}
|
||||
licenseV3Data := []*model.LicenseV3{}
|
||||
@@ -73,35 +60,6 @@ func (r *Repo) GetLicensesV3(ctx context.Context) ([]*model.LicenseV3, error) {
|
||||
return licenseV3Data, nil
|
||||
}
|
||||
|
||||
func (r *Repo) GetActiveLicenseV2(ctx context.Context) (*model.License, *basemodel.ApiError) {
|
||||
var err error
|
||||
licenses := []model.License{}
|
||||
|
||||
query := "SELECT key, activationId, planDetails, validationMessage FROM licenses"
|
||||
|
||||
err = r.db.Select(&licenses, query)
|
||||
if err != nil {
|
||||
return nil, basemodel.InternalError(fmt.Errorf("failed to get active licenses from db: %v", err))
|
||||
}
|
||||
|
||||
var active *model.License
|
||||
for _, l := range licenses {
|
||||
l.ParsePlan()
|
||||
if active == nil &&
|
||||
(l.ValidFrom != 0) &&
|
||||
(l.ValidUntil == -1 || l.ValidUntil > time.Now().Unix()) {
|
||||
active = &l
|
||||
}
|
||||
if active != nil &&
|
||||
l.ValidFrom > active.ValidFrom &&
|
||||
(l.ValidUntil == -1 || l.ValidUntil > time.Now().Unix()) {
|
||||
active = &l
|
||||
}
|
||||
}
|
||||
|
||||
return active, nil
|
||||
}
|
||||
|
||||
// GetActiveLicense fetches the latest active license from DB.
|
||||
// If the license is not present, expect a nil license and a nil error in the output.
|
||||
func (r *Repo) GetActiveLicense(ctx context.Context) (*model.License, *basemodel.ApiError) {
|
||||
@@ -156,50 +114,56 @@ func (r *Repo) GetActiveLicenseV3(ctx context.Context) (*model.LicenseV3, error)
|
||||
return active, nil
|
||||
}
|
||||
|
||||
// InsertLicense inserts a new license in db
|
||||
func (r *Repo) InsertLicense(ctx context.Context, l *model.License) error {
|
||||
// InsertLicenseV3 inserts a new license v3 in db
|
||||
func (r *Repo) InsertLicenseV3(ctx context.Context, l *model.LicenseV3) *model.ApiError {
|
||||
|
||||
if l.Key == "" {
|
||||
return fmt.Errorf("insert license failed: license key is required")
|
||||
query := `INSERT INTO licenses_v3 (id, key, data) VALUES ($1, $2, $3)`
|
||||
|
||||
// licsense is the entity of zeus so putting the entire license here without defining schema
|
||||
licenseData, err := json.Marshal(l.Data)
|
||||
if err != nil {
|
||||
return &model.ApiError{Typ: basemodel.ErrorBadData, Err: err}
|
||||
}
|
||||
|
||||
query := `INSERT INTO licenses
|
||||
(key, planDetails, activationId, validationmessage)
|
||||
VALUES ($1, $2, $3, $4)`
|
||||
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
_, err = r.db.ExecContext(ctx,
|
||||
query,
|
||||
l.ID,
|
||||
l.Key,
|
||||
l.PlanDetails,
|
||||
l.ActivationId,
|
||||
l.ValidationMessage)
|
||||
string(licenseData),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
if sqliteErr, ok := err.(sqlite3.Error); ok {
|
||||
if sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique {
|
||||
zap.L().Error("error in inserting license data: ", zap.Error(sqliteErr))
|
||||
return &model.ApiError{Typ: model.ErrorConflict, Err: sqliteErr}
|
||||
}
|
||||
}
|
||||
zap.L().Error("error in inserting license data: ", zap.Error(err))
|
||||
return fmt.Errorf("failed to insert license in db: %v", err)
|
||||
return &model.ApiError{Typ: basemodel.ErrorExec, Err: err}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdatePlanDetails writes new plan details to the db
|
||||
func (r *Repo) UpdatePlanDetails(ctx context.Context,
|
||||
key,
|
||||
planDetails string) error {
|
||||
// UpdateLicenseV3 updates a new license v3 in db
|
||||
func (r *Repo) UpdateLicenseV3(ctx context.Context, l *model.LicenseV3) error {
|
||||
|
||||
if key == "" {
|
||||
return fmt.Errorf("update plan details failed: license key is required")
|
||||
// the key and id for the license can't change so only update the data here!
|
||||
query := `UPDATE licenses_v3 SET data=$1 WHERE id=$2;`
|
||||
|
||||
license, err := json.Marshal(l.Data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert license failed: license marshal error")
|
||||
}
|
||||
|
||||
query := `UPDATE licenses
|
||||
SET planDetails = $1,
|
||||
updatedAt = $2
|
||||
WHERE key = $3`
|
||||
|
||||
_, err := r.db.ExecContext(ctx, query, planDetails, time.Now(), key)
|
||||
_, err = r.db.ExecContext(ctx,
|
||||
query,
|
||||
license,
|
||||
l.ID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
zap.L().Error("error in updating license: ", zap.Error(err))
|
||||
zap.L().Error("error in updating license data: ", zap.Error(err))
|
||||
return fmt.Errorf("failed to update license in db: %v", err)
|
||||
}
|
||||
|
||||
@@ -281,59 +245,3 @@ func (r *Repo) InitFeatures(req basemodel.FeatureSet) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InsertLicenseV3 inserts a new license v3 in db
|
||||
func (r *Repo) InsertLicenseV3(ctx context.Context, l *model.LicenseV3) *model.ApiError {
|
||||
|
||||
query := `INSERT INTO licenses_v3 (id, key, data) VALUES ($1, $2, $3)`
|
||||
|
||||
// licsense is the entity of zeus so putting the entire license here without defining schema
|
||||
licenseData, err := json.Marshal(l.Data)
|
||||
if err != nil {
|
||||
return &model.ApiError{Typ: basemodel.ErrorBadData, Err: err}
|
||||
}
|
||||
|
||||
_, err = r.db.ExecContext(ctx,
|
||||
query,
|
||||
l.ID,
|
||||
l.Key,
|
||||
string(licenseData),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
if sqliteErr, ok := err.(sqlite3.Error); ok {
|
||||
if sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique {
|
||||
zap.L().Error("error in inserting license data: ", zap.Error(sqliteErr))
|
||||
return &model.ApiError{Typ: model.ErrorConflict, Err: sqliteErr}
|
||||
}
|
||||
}
|
||||
zap.L().Error("error in inserting license data: ", zap.Error(err))
|
||||
return &model.ApiError{Typ: basemodel.ErrorExec, Err: err}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateLicenseV3 updates a new license v3 in db
|
||||
func (r *Repo) UpdateLicenseV3(ctx context.Context, l *model.LicenseV3) error {
|
||||
|
||||
// the key and id for the license can't change so only update the data here!
|
||||
query := `UPDATE licenses_v3 SET data=$1 WHERE id=$2;`
|
||||
|
||||
license, err := json.Marshal(l.Data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert license failed: license marshal error")
|
||||
}
|
||||
_, err = r.db.ExecContext(ctx,
|
||||
query,
|
||||
license,
|
||||
l.ID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
zap.L().Error("error in updating license data: ", zap.Error(err))
|
||||
return fmt.Errorf("failed to update license in db: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -27,26 +27,19 @@ var LM *Manager
|
||||
var validationFrequency = 24 * 60 * time.Minute
|
||||
|
||||
type Manager struct {
|
||||
repo *Repo
|
||||
mutex sync.Mutex
|
||||
|
||||
repo *Repo
|
||||
mutex sync.Mutex
|
||||
validatorRunning bool
|
||||
|
||||
// end the license validation, this is important to gracefully
|
||||
// stopping validation and protect in-consistent updates
|
||||
done chan struct{}
|
||||
|
||||
// terminated waits for the validate go routine to end
|
||||
terminated chan struct{}
|
||||
|
||||
// last time the license was validated
|
||||
lastValidated int64
|
||||
|
||||
// keep track of validation failure attempts
|
||||
failedAttempts uint64
|
||||
|
||||
// keep track of active license and features
|
||||
activeLicense *model.License
|
||||
activeLicenseV3 *model.LicenseV3
|
||||
activeFeatures basemodel.FeatureSet
|
||||
}
|
||||
@@ -58,7 +51,6 @@ func StartManager(db *sqlx.DB, features ...basemodel.Feature) (*Manager, error)
|
||||
|
||||
repo := NewLicenseRepo(db)
|
||||
err := repo.InitDB(db)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initiate license repo: %v", err)
|
||||
}
|
||||
@@ -66,10 +58,10 @@ func StartManager(db *sqlx.DB, features ...basemodel.Feature) (*Manager, error)
|
||||
m := &Manager{
|
||||
repo: &repo,
|
||||
}
|
||||
|
||||
if err := m.start(features...); err != nil {
|
||||
return m, err
|
||||
}
|
||||
|
||||
LM = m
|
||||
return m, nil
|
||||
}
|
||||
@@ -119,6 +111,7 @@ func (lm *Manager) LoadActiveLicenseV3(features ...basemodel.Feature) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if active != nil {
|
||||
lm.SetActiveV3(active, features...)
|
||||
} else {
|
||||
@@ -136,32 +129,6 @@ func (lm *Manager) LoadActiveLicenseV3(features ...basemodel.Feature) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lm *Manager) GetLicenses(ctx context.Context) (response []model.License, apiError *model.ApiError) {
|
||||
|
||||
licenses, err := lm.repo.GetLicenses(ctx)
|
||||
if err != nil {
|
||||
return nil, model.InternalError(err)
|
||||
}
|
||||
|
||||
for _, l := range licenses {
|
||||
l.ParsePlan()
|
||||
|
||||
if lm.activeLicense != nil && l.Key == lm.activeLicense.Key {
|
||||
l.IsCurrent = true
|
||||
}
|
||||
|
||||
if l.ValidUntil == -1 {
|
||||
// for subscriptions, there is no end-date as such
|
||||
// but for showing user some validity we default one year timespan
|
||||
l.ValidUntil = l.ValidFrom + 31556926
|
||||
}
|
||||
|
||||
response = append(response, l)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (lm *Manager) GetLicensesV3(ctx context.Context) (response []*model.LicenseV3, apiError *model.ApiError) {
|
||||
|
||||
licenses, err := lm.repo.GetLicensesV3(ctx)
|
||||
@@ -188,11 +155,11 @@ func (lm *Manager) GetLicensesV3(ctx context.Context) (response []*model.License
|
||||
func (lm *Manager) ValidatorV3(ctx context.Context) {
|
||||
zap.L().Info("ValidatorV3 started!")
|
||||
defer close(lm.terminated)
|
||||
|
||||
tick := time.NewTicker(validationFrequency)
|
||||
defer tick.Stop()
|
||||
|
||||
lm.ValidateV3(ctx)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-lm.done:
|
||||
@@ -238,10 +205,27 @@ func (lm *Manager) ValidateV3(ctx context.Context) (reterr error) {
|
||||
lm.lastValidated = time.Now().Unix()
|
||||
if reterr != nil {
|
||||
zap.L().Error("License validation completed with error", zap.Error(reterr))
|
||||
|
||||
atomic.AddUint64(&lm.failedAttempts, 1)
|
||||
// default to basic plan if validation fails for three consecutive times
|
||||
if atomic.LoadUint64(&lm.failedAttempts) > 3 {
|
||||
zap.L().Error("License validation completed with error for three consecutive times, defaulting to basic plan", zap.String("license_id", lm.activeLicenseV3.ID), zap.Bool("license_validation", false))
|
||||
lm.activeLicenseV3 = nil
|
||||
lm.activeFeatures = model.BasicPlan
|
||||
setDefaultFeatures(lm)
|
||||
err := lm.InitFeatures(lm.activeFeatures)
|
||||
if err != nil {
|
||||
zap.L().Error("Couldn't initialize features", zap.Error(err))
|
||||
}
|
||||
lm.done <- struct{}{}
|
||||
lm.validatorRunning = false
|
||||
}
|
||||
|
||||
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_CHECK_FAILED,
|
||||
map[string]interface{}{"err": reterr.Error()}, "", true, false)
|
||||
} else {
|
||||
// reset the failed attempts counter
|
||||
atomic.StoreUint64(&lm.failedAttempts, 0)
|
||||
zap.L().Info("License validation completed with no errors")
|
||||
}
|
||||
|
||||
|
||||
@@ -141,6 +141,10 @@ func main() {
|
||||
envprovider.NewFactory(),
|
||||
fileprovider.NewFactory(),
|
||||
},
|
||||
}, signoz.DeprecatedFlags{
|
||||
MaxIdleConns: maxIdleConns,
|
||||
MaxOpenConns: maxOpenConns,
|
||||
DialTimeout: dialTimeout,
|
||||
})
|
||||
if err != nil {
|
||||
zap.L().Fatal("Failed to create config", zap.Error(err))
|
||||
@@ -161,9 +165,6 @@ func main() {
|
||||
PrivateHostPort: baseconst.PrivateHostPort,
|
||||
DisableRules: disableRules,
|
||||
RuleRepoURL: ruleRepoURL,
|
||||
MaxIdleConns: maxIdleConns,
|
||||
MaxOpenConns: maxOpenConns,
|
||||
DialTimeout: dialTimeout,
|
||||
CacheConfigPath: cacheConfigPath,
|
||||
FluxInterval: fluxInterval,
|
||||
FluxIntervalForTraceDetail: fluxIntervalForTraceDetail,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
@@ -61,37 +60,6 @@ type LicensePlan struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
func (l *License) ParsePlan() error {
|
||||
l.LicensePlan = LicensePlan{}
|
||||
|
||||
planData, err := base64.StdEncoding.DecodeString(l.PlanDetails)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
plan := LicensePlan{}
|
||||
err = json.Unmarshal([]byte(planData), &plan)
|
||||
if err != nil {
|
||||
l.ValidationMessage = "failed to parse plan from license"
|
||||
return errors.Wrap(err, "failed to parse plan from license")
|
||||
}
|
||||
|
||||
l.LicensePlan = plan
|
||||
l.ParseFeatures()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *License) ParseFeatures() {
|
||||
switch l.PlanKey {
|
||||
case Pro:
|
||||
l.FeatureSet = ProPlan
|
||||
case Enterprise:
|
||||
l.FeatureSet = EnterprisePlan
|
||||
default:
|
||||
l.FeatureSet = BasicPlan
|
||||
}
|
||||
}
|
||||
|
||||
type Licenses struct {
|
||||
TrialStart int64 `json:"trialStart"`
|
||||
TrialEnd int64 `json:"trialEnd"`
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
@@ -46,9 +45,9 @@ type Manager struct {
|
||||
tenantID string
|
||||
}
|
||||
|
||||
func New(modelDao dao.ModelDao, licenseRepo *license.Repo, clickhouseConn clickhouse.Conn) (*Manager, error) {
|
||||
func New(modelDao dao.ModelDao, licenseRepo *license.Repo, clickhouseConn clickhouse.Conn, chUrl string) (*Manager, error) {
|
||||
hostNameRegex := regexp.MustCompile(`tcp://(?P<hostname>.*):`)
|
||||
hostNameRegexMatches := hostNameRegex.FindStringSubmatch(os.Getenv("ClickHouseUrl"))
|
||||
hostNameRegexMatches := hostNameRegex.FindStringSubmatch(chUrl)
|
||||
|
||||
tenantID := ""
|
||||
if len(hostNameRegexMatches) == 2 {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import axios from 'api';
|
||||
import { ApiV3Instance as axios } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiV2Instance as axios } from 'api';
|
||||
import { ApiV3Instance as axios } from 'api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps } from 'types/api/licenses/getAll';
|
||||
|
||||
|
||||
@@ -259,7 +259,7 @@ function AnomalyAlertEvaluationView({
|
||||
grid: {
|
||||
show: true,
|
||||
},
|
||||
axes: getAxes(isDarkMode, yAxisUnit),
|
||||
axes: getAxes({ isDarkMode, yAxisUnit }),
|
||||
tzDate: (timestamp: number): Date =>
|
||||
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
|
||||
};
|
||||
|
||||
@@ -122,7 +122,7 @@ export function BillingUsageGraph(props: BillingUsageGraphProps): JSX.Element {
|
||||
[graphCompatibleData.data.result],
|
||||
);
|
||||
|
||||
const axesOptions = getAxes(isDarkMode, '');
|
||||
const axesOptions = getAxes({ isDarkMode, yAxisUnit: '' });
|
||||
|
||||
const optionsForChart: uPlot.Options = useMemo(
|
||||
() => ({
|
||||
|
||||
@@ -1,34 +1,26 @@
|
||||
import './InfraMonitoring.styles.scss';
|
||||
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Skeleton,
|
||||
Spin,
|
||||
Table,
|
||||
TablePaginationConfig,
|
||||
TableProps,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { SorterResult } from 'antd/es/table/interface';
|
||||
import { VerticalAlignTopOutlined } from '@ant-design/icons';
|
||||
import { Button, Tooltip, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { HostListPayload } from 'api/infraMonitoring/getHostLists';
|
||||
import HostMetricDetail from 'components/HostMetricsDetail';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import { usePageSize } from 'container/InfraMonitoringK8s/utils';
|
||||
import { useGetHostList } from 'hooks/infraMonitoring/useGetHostList';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { Filter } from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import HostsEmptyOrIncorrectMetrics from './HostsEmptyOrIncorrectMetrics';
|
||||
import HostsListControls from './HostsListControls';
|
||||
import {
|
||||
formatDataForTable,
|
||||
getHostListsQuery,
|
||||
getHostsListColumns,
|
||||
HostRowData,
|
||||
} from './utils';
|
||||
import HostsListTable from './HostsListTable';
|
||||
import { getHostListsQuery, HostsQuickFiltersConfig } from './utils';
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function HostsList(): JSX.Element {
|
||||
@@ -41,6 +33,7 @@ function HostsList(): JSX.Element {
|
||||
items: [],
|
||||
op: 'and',
|
||||
});
|
||||
const [showFilters, setShowFilters] = useState<boolean>(true);
|
||||
|
||||
const [orderBy, setOrderBy] = useState<{
|
||||
columnName: string;
|
||||
@@ -72,55 +65,24 @@ function HostsList(): JSX.Element {
|
||||
},
|
||||
);
|
||||
|
||||
const sentAnyHostMetricsData = useMemo(
|
||||
() => data?.payload?.data?.sentAnyHostMetricsData || false,
|
||||
[data],
|
||||
);
|
||||
|
||||
const isSendingIncorrectK8SAgentMetrics = useMemo(
|
||||
() => data?.payload?.data?.isSendingK8SAgentMetrics || false,
|
||||
[data],
|
||||
);
|
||||
|
||||
const hostMetricsData = useMemo(() => data?.payload?.data?.records || [], [
|
||||
data,
|
||||
]);
|
||||
const totalCount = data?.payload?.data?.total || 0;
|
||||
|
||||
const formattedHostMetricsData = useMemo(
|
||||
() => formatDataForTable(hostMetricsData),
|
||||
[hostMetricsData],
|
||||
);
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const columns = useMemo(() => getHostsListColumns(), []);
|
||||
|
||||
const handleTableChange: TableProps<HostRowData>['onChange'] = useCallback(
|
||||
(
|
||||
pagination: TablePaginationConfig,
|
||||
_filters: Record<string, (string | number | boolean)[] | null>,
|
||||
sorter: SorterResult<HostRowData> | SorterResult<HostRowData>[],
|
||||
): void => {
|
||||
if (pagination.current) {
|
||||
setCurrentPage(pagination.current);
|
||||
}
|
||||
|
||||
if ('field' in sorter && sorter.order) {
|
||||
setOrderBy({
|
||||
columnName: sorter.field as string,
|
||||
order: sorter.order === 'ascend' ? 'asc' : 'desc',
|
||||
});
|
||||
} else {
|
||||
setOrderBy(null);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query: currentQuery.builder.queryData[0],
|
||||
entityVersion: '',
|
||||
});
|
||||
|
||||
const handleFiltersChange = useCallback(
|
||||
(value: IBuilderQuery['filters']): void => {
|
||||
const isNewFilterAdded = value.items.length !== filters.items.length;
|
||||
setFilters(value);
|
||||
handleChangeQueryData('filters', value);
|
||||
if (isNewFilterAdded) {
|
||||
setFilters(value);
|
||||
setCurrentPage(1);
|
||||
|
||||
logEvent('Infra Monitoring: Hosts list filters applied', {
|
||||
@@ -128,6 +90,7 @@ function HostsList(): JSX.Element {
|
||||
});
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[filters],
|
||||
);
|
||||
|
||||
@@ -142,118 +105,73 @@ function HostsList(): JSX.Element {
|
||||
);
|
||||
}, [selectedHostName, hostMetricsData]);
|
||||
|
||||
const handleRowClick = (record: HostRowData): void => {
|
||||
setSelectedHostName(record.hostName);
|
||||
|
||||
logEvent('Infra Monitoring: Hosts list item clicked', {
|
||||
host: record.hostName,
|
||||
});
|
||||
};
|
||||
|
||||
const handleCloseHostDetail = (): void => {
|
||||
setSelectedHostName(null);
|
||||
};
|
||||
|
||||
const showHostsTable =
|
||||
!isError &&
|
||||
sentAnyHostMetricsData &&
|
||||
!isSendingIncorrectK8SAgentMetrics &&
|
||||
!(formattedHostMetricsData.length === 0 && filters.items.length > 0);
|
||||
const handleFilterVisibilityChange = (): void => {
|
||||
setShowFilters(!showFilters);
|
||||
};
|
||||
|
||||
const showNoFilteredHostsMessage =
|
||||
!isFetching &&
|
||||
!isLoading &&
|
||||
formattedHostMetricsData.length === 0 &&
|
||||
filters.items.length > 0;
|
||||
|
||||
const showHostsEmptyState =
|
||||
!isFetching &&
|
||||
!isLoading &&
|
||||
(!sentAnyHostMetricsData || isSendingIncorrectK8SAgentMetrics) &&
|
||||
!filters.items.length;
|
||||
const handleQuickFiltersChange = (query: Query): void => {
|
||||
handleChangeQueryData('filters', query.builder.queryData[0].filters);
|
||||
handleFiltersChange(query.builder.queryData[0].filters);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="hosts-list">
|
||||
<HostsListControls handleFiltersChange={handleFiltersChange} />
|
||||
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
|
||||
|
||||
{showHostsEmptyState && (
|
||||
<HostsEmptyOrIncorrectMetrics
|
||||
noData={!sentAnyHostMetricsData}
|
||||
incorrectData={isSendingIncorrectK8SAgentMetrics}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showNoFilteredHostsMessage && (
|
||||
<div className="no-filtered-hosts-message-container">
|
||||
<div className="no-filtered-hosts-message-content">
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className="empty-state-svg"
|
||||
<div className="hosts-list-content">
|
||||
{showFilters && (
|
||||
<div className="hosts-quick-filters-container">
|
||||
<div className="hosts-quick-filters-container-header">
|
||||
<Typography.Text>Filters</Typography.Text>
|
||||
<Tooltip title="Collapse Filters">
|
||||
<VerticalAlignTopOutlined
|
||||
rotate={270}
|
||||
onClick={handleFilterVisibilityChange}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.INFRA_MONITORING}
|
||||
config={HostsQuickFiltersConfig}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
onFilterChange={handleQuickFiltersChange}
|
||||
/>
|
||||
|
||||
<Typography.Text className="no-filtered-hosts-message">
|
||||
This query had no results. Edit your query and try again!
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(isFetching || isLoading) && (
|
||||
<div className="hosts-list-loading-state">
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
/>
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
/>
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
)}
|
||||
<div className="hosts-list-table-container">
|
||||
<div className="hosts-list-table-header">
|
||||
{!showFilters && (
|
||||
<div className="quick-filters-toggle-container">
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={handleFilterVisibilityChange}
|
||||
>
|
||||
<Filter size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<HostsListControls handleFiltersChange={handleFiltersChange} />
|
||||
</div>
|
||||
<HostsListTable
|
||||
isLoading={isLoading}
|
||||
isFetching={isFetching}
|
||||
isError={isError}
|
||||
tableData={data}
|
||||
hostMetricsData={hostMetricsData}
|
||||
filters={filters}
|
||||
currentPage={currentPage}
|
||||
setCurrentPage={setCurrentPage}
|
||||
setSelectedHostName={setSelectedHostName}
|
||||
pageSize={pageSize}
|
||||
setPageSize={setPageSize}
|
||||
setOrderBy={setOrderBy}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showHostsTable && (
|
||||
<Table
|
||||
className="hosts-list-table"
|
||||
dataSource={isFetching || isLoading ? [] : formattedHostMetricsData}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
pageSize,
|
||||
total: totalCount,
|
||||
showSizeChanger: true,
|
||||
hideOnSinglePage: false,
|
||||
onChange: (page, pageSize): void => {
|
||||
setCurrentPage(page);
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
loading={{
|
||||
spinning: isFetching || isLoading,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
rowKey={(record): string => record.hostName}
|
||||
onChange={handleTableChange}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
<HostMetricDetail
|
||||
host={selectedHostData}
|
||||
isModalTimeSelection
|
||||
|
||||
183
frontend/src/container/InfraMonitoringHosts/HostsListTable.tsx
Normal file
183
frontend/src/container/InfraMonitoringHosts/HostsListTable.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Skeleton,
|
||||
Spin,
|
||||
Table,
|
||||
TablePaginationConfig,
|
||||
TableProps,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { SorterResult } from 'antd/es/table/interface';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import HostsEmptyOrIncorrectMetrics from './HostsEmptyOrIncorrectMetrics';
|
||||
import {
|
||||
formatDataForTable,
|
||||
getHostsListColumns,
|
||||
HostRowData,
|
||||
HostsListTableProps,
|
||||
} from './utils';
|
||||
|
||||
export default function HostsListTable({
|
||||
isLoading,
|
||||
isFetching,
|
||||
isError,
|
||||
tableData: data,
|
||||
hostMetricsData,
|
||||
filters,
|
||||
setSelectedHostName,
|
||||
currentPage,
|
||||
setCurrentPage,
|
||||
pageSize,
|
||||
setOrderBy,
|
||||
setPageSize,
|
||||
}: HostsListTableProps): JSX.Element {
|
||||
const columns = useMemo(() => getHostsListColumns(), []);
|
||||
|
||||
const sentAnyHostMetricsData = useMemo(
|
||||
() => data?.payload?.data?.sentAnyHostMetricsData || false,
|
||||
[data],
|
||||
);
|
||||
|
||||
const isSendingIncorrectK8SAgentMetrics = useMemo(
|
||||
() => data?.payload?.data?.isSendingK8SAgentMetrics || false,
|
||||
[data],
|
||||
);
|
||||
|
||||
const formattedHostMetricsData = useMemo(
|
||||
() => formatDataForTable(hostMetricsData),
|
||||
[hostMetricsData],
|
||||
);
|
||||
|
||||
const totalCount = data?.payload?.data?.total || 0;
|
||||
|
||||
const handleTableChange: TableProps<HostRowData>['onChange'] = useCallback(
|
||||
(
|
||||
pagination: TablePaginationConfig,
|
||||
_filters: Record<string, (string | number | boolean)[] | null>,
|
||||
sorter: SorterResult<HostRowData> | SorterResult<HostRowData>[],
|
||||
): void => {
|
||||
if (pagination.current) {
|
||||
setCurrentPage(pagination.current);
|
||||
}
|
||||
|
||||
if ('field' in sorter && sorter.order) {
|
||||
setOrderBy({
|
||||
columnName: sorter.field as string,
|
||||
order: sorter.order === 'ascend' ? 'asc' : 'desc',
|
||||
});
|
||||
} else {
|
||||
setOrderBy(null);
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleRowClick = (record: HostRowData): void => {
|
||||
setSelectedHostName(record.hostName);
|
||||
logEvent('Infra Monitoring: Hosts list item clicked', {
|
||||
host: record.hostName,
|
||||
});
|
||||
};
|
||||
|
||||
const showNoFilteredHostsMessage =
|
||||
!isFetching &&
|
||||
!isLoading &&
|
||||
formattedHostMetricsData.length === 0 &&
|
||||
filters.items.length > 0;
|
||||
|
||||
const showHostsEmptyState =
|
||||
!isFetching &&
|
||||
!isLoading &&
|
||||
(!sentAnyHostMetricsData || isSendingIncorrectK8SAgentMetrics) &&
|
||||
!filters.items.length;
|
||||
|
||||
if (isError) {
|
||||
return <Typography>{data?.error || 'Something went wrong'}</Typography>;
|
||||
}
|
||||
|
||||
if (showHostsEmptyState) {
|
||||
return (
|
||||
<HostsEmptyOrIncorrectMetrics
|
||||
noData={!sentAnyHostMetricsData}
|
||||
incorrectData={isSendingIncorrectK8SAgentMetrics}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (showNoFilteredHostsMessage) {
|
||||
return (
|
||||
<div className="no-filtered-hosts-message-container">
|
||||
<div className="no-filtered-hosts-message-content">
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className="empty-state-svg"
|
||||
/>
|
||||
|
||||
<Typography.Text className="no-filtered-hosts-message">
|
||||
This query had no results. Edit your query and try again!
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading || isFetching) {
|
||||
return (
|
||||
<div className="hosts-list-loading-state">
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
/>
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
/>
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Table
|
||||
className="hosts-list-table"
|
||||
dataSource={isLoading || isFetching ? [] : formattedHostMetricsData}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
pageSize,
|
||||
total: totalCount,
|
||||
showSizeChanger: true,
|
||||
hideOnSinglePage: false,
|
||||
onChange: (page, pageSize): void => {
|
||||
setCurrentPage(page);
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
}}
|
||||
scroll={{ x: true }}
|
||||
loading={{
|
||||
spinning: isFetching || isLoading,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
rowKey={(record): string => record.hostName}
|
||||
onChange={handleTableChange}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
}
|
||||
|
||||
.hosts-list-controls {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
|
||||
display: flex;
|
||||
@@ -51,6 +52,40 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hosts-list-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.hosts-quick-filters-container {
|
||||
width: 280px;
|
||||
min-width: 280px;
|
||||
border-right: 1px solid var(--bg-slate-400);
|
||||
|
||||
.hosts-quick-filters-container-header {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--bg-slate-400);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hosts-list-table-container {
|
||||
flex: 1;
|
||||
|
||||
.hosts-list-table-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.quick-filters-toggle-container {
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hosts-list-table {
|
||||
.ant-table {
|
||||
.ant-table-thead > tr > th {
|
||||
@@ -164,7 +199,7 @@
|
||||
margin: 0;
|
||||
|
||||
// this is to offset intercom icon till we improve the design
|
||||
padding-right: 72px;
|
||||
right: 20px;
|
||||
|
||||
.ant-pagination-item {
|
||||
border-radius: 4px;
|
||||
@@ -214,6 +249,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
flex: 1;
|
||||
|
||||
.hosts-list-loading-state-item {
|
||||
height: 48px;
|
||||
@@ -222,6 +258,7 @@
|
||||
}
|
||||
|
||||
.no-filtered-hosts-message-container {
|
||||
flex: 1;
|
||||
height: 30vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -246,6 +283,7 @@
|
||||
.hosts-empty-state-container {
|
||||
padding: 16px;
|
||||
height: 40vh;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
@@ -3,9 +3,22 @@ import './InfraMonitoring.styles.scss';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Progress, TabsProps, Tag } from 'antd';
|
||||
import { ColumnType } from 'antd/es/table';
|
||||
import { HostData, HostListPayload } from 'api/infraMonitoring/getHostLists';
|
||||
import {
|
||||
HostData,
|
||||
HostListPayload,
|
||||
HostListResponse,
|
||||
} from 'api/infraMonitoring/getHostLists';
|
||||
import {
|
||||
FiltersType,
|
||||
IQuickFiltersConfig,
|
||||
} from 'components/QuickFilters/types';
|
||||
import TabLabel from 'components/TabLabel';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import HostsList from './HostsList';
|
||||
|
||||
@@ -18,6 +31,29 @@ export interface HostRowData {
|
||||
active: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface HostsListTableProps {
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
isFetching: boolean;
|
||||
tableData:
|
||||
| SuccessResponse<HostListResponse, unknown>
|
||||
| ErrorResponse
|
||||
| undefined;
|
||||
hostMetricsData: HostData[];
|
||||
filters: TagFilter;
|
||||
setSelectedHostName: Dispatch<SetStateAction<string | null>>;
|
||||
currentPage: number;
|
||||
setCurrentPage: Dispatch<SetStateAction<number>>;
|
||||
pageSize: number;
|
||||
setOrderBy: Dispatch<
|
||||
SetStateAction<{
|
||||
columnName: string;
|
||||
order: 'asc' | 'desc';
|
||||
} | null>
|
||||
>;
|
||||
setPageSize: (pageSize: number) => void;
|
||||
}
|
||||
|
||||
export const getHostListsQuery = (): HostListPayload => ({
|
||||
filters: {
|
||||
items: [],
|
||||
@@ -132,3 +168,36 @@ export const formatDataForTable = (data: HostData[]): HostRowData[] =>
|
||||
wait: `${Number((host.wait * 100).toFixed(1))}%`,
|
||||
load15: host.load15,
|
||||
}));
|
||||
|
||||
export const HostsQuickFiltersConfig: IQuickFiltersConfig[] = [
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'Host Name',
|
||||
attributeKey: {
|
||||
key: 'host_name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: 'system_cpu_load_average_15m',
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'OS Type',
|
||||
attributeKey: {
|
||||
key: 'os_type',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: 'system_cpu_load_average_15m',
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -37,6 +37,25 @@ function K8sFiltersSidePanel({
|
||||
}
|
||||
}, [searchValue]);
|
||||
|
||||
// Close side panel when clicking outside of it
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
sidePanelRef.current &&
|
||||
!sidePanelRef.current.contains(event.target as Node)
|
||||
) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="k8s-filters-side-panel-container">
|
||||
<div className="k8s-filters-side-panel" ref={sidePanelRef}>
|
||||
|
||||
@@ -88,6 +88,8 @@ function QueryBuilderSearch({
|
||||
[pathname],
|
||||
);
|
||||
|
||||
const [isEditingTag, setIsEditingTag] = useState(false);
|
||||
|
||||
const {
|
||||
updateTag,
|
||||
handleClearTag,
|
||||
@@ -133,6 +135,16 @@ function QueryBuilderSearch({
|
||||
|
||||
const { handleRunQuery, currentQuery } = useQueryBuilder();
|
||||
|
||||
const toggleEditMode = useCallback(
|
||||
(value: boolean) => {
|
||||
// Editing mode is required only in infra monitoring mode
|
||||
if (isInfraMonitoring) {
|
||||
setIsEditingTag(value);
|
||||
}
|
||||
},
|
||||
[isInfraMonitoring],
|
||||
);
|
||||
|
||||
const onTagRender = ({
|
||||
value,
|
||||
closable,
|
||||
@@ -146,12 +158,16 @@ function QueryBuilderSearch({
|
||||
|
||||
const onCloseHandler = (): void => {
|
||||
onClose();
|
||||
// Editing is done after closing a tag
|
||||
toggleEditMode(false);
|
||||
handleSearch('');
|
||||
setSearchKey('');
|
||||
};
|
||||
|
||||
const tagEditHandler = (value: string): void => {
|
||||
updateTag(value);
|
||||
// Editing starts
|
||||
toggleEditMode(true);
|
||||
if (isInfraMonitoring) {
|
||||
setSearchValue(value);
|
||||
} else {
|
||||
@@ -188,6 +204,11 @@ function QueryBuilderSearch({
|
||||
if (isMulti || event.key === 'Backspace') handleKeyDown(event);
|
||||
if (isExistsNotExistsOperator(searchValue)) handleKeyDown(event);
|
||||
|
||||
// Editing is done after enter key press
|
||||
if (event.key === 'Enter') {
|
||||
toggleEditMode(false);
|
||||
}
|
||||
|
||||
if (
|
||||
!disableNavigationShortcuts &&
|
||||
(event.ctrlKey || event.metaKey) &&
|
||||
@@ -270,7 +291,14 @@ function QueryBuilderSearch({
|
||||
};
|
||||
});
|
||||
|
||||
onChange(initialTagFilters);
|
||||
// If in infra monitoring, only run the onChange query when editing is finsished.
|
||||
if (isInfraMonitoring) {
|
||||
if (!isEditingTag) {
|
||||
onChange(initialTagFilters);
|
||||
}
|
||||
} else {
|
||||
onChange(initialTagFilters);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sourceKeys]);
|
||||
|
||||
@@ -367,7 +395,11 @@ function QueryBuilderSearch({
|
||||
)
|
||||
}
|
||||
showAction={['focus']}
|
||||
onBlur={handleOnBlur}
|
||||
onBlur={(e: React.FocusEvent<HTMLInputElement>): void => {
|
||||
handleOnBlur(e);
|
||||
// Editing is done after tapping out of the input
|
||||
toggleEditMode(false);
|
||||
}}
|
||||
popupClassName={isLogsExplorerPage ? 'logs-explorer-popup' : ''}
|
||||
dropdownRender={(menu): ReactElement => (
|
||||
<div>
|
||||
|
||||
@@ -387,6 +387,6 @@ export const getUPlotChartOptions = ({
|
||||
hiddenGraph,
|
||||
isDarkMode,
|
||||
}),
|
||||
axes: getAxes(isDarkMode, yAxisUnit),
|
||||
axes: getAxes({ isDarkMode, yAxisUnit, panelType }),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -123,6 +123,7 @@ export const getUplotHistogramChartOptions = ({
|
||||
setGraphsVisibilityStates,
|
||||
mergeAllQueries,
|
||||
onClickHandler = _noop,
|
||||
panelType,
|
||||
}: GetUplotHistogramChartOptionsProps): uPlot.Options =>
|
||||
({
|
||||
id,
|
||||
@@ -210,5 +211,5 @@ export const getUplotHistogramChartOptions = ({
|
||||
},
|
||||
],
|
||||
},
|
||||
axes: getAxes(isDarkMode),
|
||||
axes: getAxes({ isDarkMode, panelType }),
|
||||
} as uPlot.Options);
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
// @ts-nocheck
|
||||
import { getToolTipValue } from 'components/Graph/yAxisConfig';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
|
||||
import { uPlotXAxisValuesFormat } from './constants';
|
||||
import getGridColor from './getGridColor';
|
||||
|
||||
const PANEL_TYPES_WITH_X_AXIS_DATETIME_FORMAT = [
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
PANEL_TYPES.BAR,
|
||||
PANEL_TYPES.PIE,
|
||||
];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const getAxes = (isDarkMode: boolean, yAxisUnit?: string): any => [
|
||||
const getAxes = ({
|
||||
isDarkMode,
|
||||
yAxisUnit,
|
||||
panelType,
|
||||
}: {
|
||||
isDarkMode: boolean;
|
||||
yAxisUnit?: string;
|
||||
panelType?: PANEL_TYPES;
|
||||
}): any => [
|
||||
{
|
||||
stroke: isDarkMode ? 'white' : 'black', // Color of the axis line
|
||||
grid: {
|
||||
@@ -19,7 +34,11 @@ const getAxes = (isDarkMode: boolean, yAxisUnit?: string): any => [
|
||||
width: 0.3, // Width of the tick lines,
|
||||
show: true,
|
||||
},
|
||||
values: uPlotXAxisValuesFormat,
|
||||
...(PANEL_TYPES_WITH_X_AXIS_DATETIME_FORMAT.includes(panelType)
|
||||
? {
|
||||
values: uPlotXAxisValuesFormat,
|
||||
}
|
||||
: {}),
|
||||
gap: 5,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -43,7 +43,7 @@ function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
|
||||
</div>
|
||||
</div>
|
||||
<div className="bottom-section">
|
||||
<AlertSeverity severity="warning" />
|
||||
{labels.severity && <AlertSeverity severity={labels.severity} />}
|
||||
|
||||
{/* // TODO(shaheer): Get actual data when we are able to get alert firing from state from API */}
|
||||
{/* <AlertStatus
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package clickhouseReader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/ClickHouse/clickhouse-go/v2"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Encoding string
|
||||
@@ -18,7 +16,6 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
defaultDatasource string = "tcp://localhost:9000"
|
||||
defaultTraceDB string = "signoz_traces"
|
||||
defaultOperationsTable string = "distributed_signoz_operations"
|
||||
defaultIndexTable string = "distributed_signoz_index_v2"
|
||||
@@ -58,9 +55,6 @@ type namespaceConfig struct {
|
||||
namespace string
|
||||
Enabled bool
|
||||
Datasource string
|
||||
MaxIdleConns int
|
||||
MaxOpenConns int
|
||||
DialTimeout time.Duration
|
||||
TraceDB string
|
||||
OperationsTable string
|
||||
IndexTable string
|
||||
@@ -99,37 +93,6 @@ type namespaceConfig struct {
|
||||
// Connecto defines how to connect to the database
|
||||
type Connector func(cfg *namespaceConfig) (clickhouse.Conn, error)
|
||||
|
||||
func defaultConnector(cfg *namespaceConfig) (clickhouse.Conn, error) {
|
||||
ctx := context.Background()
|
||||
options, err := clickhouse.ParseDSN(cfg.Datasource)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if the DSN contained any of the following options, if not set from configuration
|
||||
if options.MaxIdleConns == 0 {
|
||||
options.MaxIdleConns = cfg.MaxIdleConns
|
||||
}
|
||||
if options.MaxOpenConns == 0 {
|
||||
options.MaxOpenConns = cfg.MaxOpenConns
|
||||
}
|
||||
if options.DialTimeout == 0 {
|
||||
options.DialTimeout = cfg.DialTimeout
|
||||
}
|
||||
|
||||
zap.L().Info("Connecting to Clickhouse", zap.String("at", options.Addr[0]), zap.Int("MaxIdleConns", options.MaxIdleConns), zap.Int("MaxOpenConns", options.MaxOpenConns), zap.Duration("DialTimeout", options.DialTimeout))
|
||||
db, err := clickhouse.Open(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := db.Ping(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// Options store storage plugin related configs
|
||||
type Options struct {
|
||||
primary *namespaceConfig
|
||||
@@ -139,26 +102,13 @@ type Options struct {
|
||||
|
||||
// NewOptions creates a new Options struct.
|
||||
func NewOptions(
|
||||
datasource string,
|
||||
maxIdleConns int,
|
||||
maxOpenConns int,
|
||||
dialTimeout time.Duration,
|
||||
primaryNamespace string,
|
||||
otherNamespaces ...string,
|
||||
) *Options {
|
||||
|
||||
if datasource == "" {
|
||||
datasource = defaultDatasource
|
||||
}
|
||||
|
||||
options := &Options{
|
||||
primary: &namespaceConfig{
|
||||
namespace: primaryNamespace,
|
||||
Enabled: true,
|
||||
Datasource: datasource,
|
||||
MaxIdleConns: maxIdleConns,
|
||||
MaxOpenConns: maxOpenConns,
|
||||
DialTimeout: dialTimeout,
|
||||
TraceDB: defaultTraceDB,
|
||||
OperationsTable: defaultOperationsTable,
|
||||
IndexTable: defaultIndexTable,
|
||||
@@ -181,7 +131,6 @@ func NewOptions(
|
||||
WriteBatchDelay: defaultWriteBatchDelay,
|
||||
WriteBatchSize: defaultWriteBatchSize,
|
||||
Encoding: defaultEncoding,
|
||||
Connector: defaultConnector,
|
||||
|
||||
LogsTableV2: defaultLogsTableV2,
|
||||
LogsLocalTableV2: defaultLogsLocalTableV2,
|
||||
@@ -200,7 +149,6 @@ func NewOptions(
|
||||
if namespace == archiveNamespace {
|
||||
options.others[namespace] = &namespaceConfig{
|
||||
namespace: namespace,
|
||||
Datasource: datasource,
|
||||
TraceDB: "",
|
||||
OperationsTable: "",
|
||||
IndexTable: "",
|
||||
@@ -214,7 +162,6 @@ func NewOptions(
|
||||
WriteBatchDelay: defaultWriteBatchDelay,
|
||||
WriteBatchSize: defaultWriteBatchSize,
|
||||
Encoding: defaultEncoding,
|
||||
Connector: defaultConnector,
|
||||
}
|
||||
} else {
|
||||
options.others[namespace] = &namespaceConfig{namespace: namespace}
|
||||
|
||||
@@ -166,26 +166,16 @@ type ClickHouseReader struct {
|
||||
// NewTraceReader returns a TraceReader for the database
|
||||
func NewReader(
|
||||
localDB *sqlx.DB,
|
||||
db driver.Conn,
|
||||
configFile string,
|
||||
featureFlag interfaces.FeatureLookup,
|
||||
maxIdleConns int,
|
||||
maxOpenConns int,
|
||||
dialTimeout time.Duration,
|
||||
cluster string,
|
||||
useLogsNewSchema bool,
|
||||
useTraceNewSchema bool,
|
||||
fluxIntervalForTraceDetail time.Duration,
|
||||
cache cache.Cache,
|
||||
) *ClickHouseReader {
|
||||
|
||||
datasource := os.Getenv("ClickHouseUrl")
|
||||
options := NewOptions(datasource, maxIdleConns, maxOpenConns, dialTimeout, primaryNamespace, archiveNamespace)
|
||||
db, err := initialize(options)
|
||||
|
||||
if err != nil {
|
||||
zap.L().Fatal("failed to initialize ClickHouse", zap.Error(err))
|
||||
}
|
||||
|
||||
options := NewOptions(primaryNamespace, archiveNamespace)
|
||||
return NewReaderFromClickhouseConnection(db, options, localDB, configFile, featureFlag, cluster, useLogsNewSchema, useTraceNewSchema, fluxIntervalForTraceDetail, cache)
|
||||
}
|
||||
|
||||
@@ -208,29 +198,6 @@ func NewReaderFromClickhouseConnection(
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
regex := os.Getenv("ClickHouseOptimizeReadInOrderRegex")
|
||||
var regexCompiled *regexp.Regexp
|
||||
if regex != "" {
|
||||
regexCompiled, err = regexp.Compile(regex)
|
||||
if err != nil {
|
||||
zap.L().Error("Incorrect regex for ClickHouseOptimizeReadInOrderRegex")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
wrap := clickhouseConnWrapper{
|
||||
conn: db,
|
||||
settings: ClickhouseQuerySettings{
|
||||
MaxExecutionTime: os.Getenv("ClickHouseMaxExecutionTime"),
|
||||
MaxExecutionTimeLeaf: os.Getenv("ClickHouseMaxExecutionTimeLeaf"),
|
||||
TimeoutBeforeCheckingExecutionSpeed: os.Getenv("ClickHouseTimeoutBeforeCheckingExecutionSpeed"),
|
||||
MaxBytesToRead: os.Getenv("ClickHouseMaxBytesToRead"),
|
||||
OptimizeReadInOrderRegex: os.Getenv("ClickHouseOptimizeReadInOrderRegex"),
|
||||
OptimizeReadInOrderRegexCompiled: regexCompiled,
|
||||
MaxResultRowsForCHQuery: constants.MaxResultRowsForCHQuery,
|
||||
},
|
||||
}
|
||||
|
||||
logsTableName := options.primary.LogsTable
|
||||
logsLocalTableName := options.primary.LogsLocalTable
|
||||
if useLogsNewSchema {
|
||||
@@ -246,7 +213,7 @@ func NewReaderFromClickhouseConnection(
|
||||
}
|
||||
|
||||
return &ClickHouseReader{
|
||||
db: wrap,
|
||||
db: db,
|
||||
localDB: localDB,
|
||||
TraceDB: options.primary.TraceDB,
|
||||
alertManager: alertManager,
|
||||
@@ -438,28 +405,6 @@ func reloadConfig(filename string, logger log.Logger, rls ...func(*config.Config
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
func initialize(options *Options) (clickhouse.Conn, error) {
|
||||
|
||||
db, err := connect(options.getPrimary())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error connecting to primary db: %v", err)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func connect(cfg *namespaceConfig) (clickhouse.Conn, error) {
|
||||
if cfg.Encoding != EncodingJSON && cfg.Encoding != EncodingProto {
|
||||
return nil, fmt.Errorf("unknown encoding %q, supported: %q, %q", cfg.Encoding, EncodingJSON, EncodingProto)
|
||||
}
|
||||
|
||||
return cfg.Connector(cfg)
|
||||
}
|
||||
|
||||
func (r *ClickHouseReader) GetConn() clickhouse.Conn {
|
||||
return r.db
|
||||
}
|
||||
|
||||
func (r *ClickHouseReader) GetInstantQueryMetricsResult(ctx context.Context, queryParams *model.InstantQueryMetricsParams) (*promql.Result, *stats.QueryStats, *model.ApiError) {
|
||||
qry, err := r.queryEngine.NewInstantQuery(ctx, r.remoteStorage, nil, queryParams.Query, queryParams.Time)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
package clickhouseReader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"regexp"
|
||||
|
||||
"github.com/ClickHouse/clickhouse-go/v2"
|
||||
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
|
||||
"go.signoz.io/signoz/pkg/query-service/common"
|
||||
)
|
||||
|
||||
type ClickhouseQuerySettings struct {
|
||||
MaxExecutionTime string
|
||||
MaxExecutionTimeLeaf string
|
||||
TimeoutBeforeCheckingExecutionSpeed string
|
||||
MaxBytesToRead string
|
||||
OptimizeReadInOrderRegex string
|
||||
OptimizeReadInOrderRegexCompiled *regexp.Regexp
|
||||
MaxResultRowsForCHQuery int
|
||||
}
|
||||
|
||||
type clickhouseConnWrapper struct {
|
||||
conn clickhouse.Conn
|
||||
settings ClickhouseQuerySettings
|
||||
}
|
||||
|
||||
func (c clickhouseConnWrapper) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
func (c clickhouseConnWrapper) Ping(ctx context.Context) error {
|
||||
return c.conn.Ping(ctx)
|
||||
}
|
||||
|
||||
func (c clickhouseConnWrapper) Stats() driver.Stats {
|
||||
return c.conn.Stats()
|
||||
}
|
||||
|
||||
func (c clickhouseConnWrapper) addClickHouseSettings(ctx context.Context, query string) context.Context {
|
||||
settings := clickhouse.Settings{}
|
||||
|
||||
logComment := c.getLogComment(ctx)
|
||||
if logComment != "" {
|
||||
settings["log_comment"] = logComment
|
||||
}
|
||||
|
||||
if ctx.Value("enforce_max_result_rows") != nil {
|
||||
settings["max_result_rows"] = c.settings.MaxResultRowsForCHQuery
|
||||
}
|
||||
|
||||
if c.settings.MaxBytesToRead != "" {
|
||||
settings["max_bytes_to_read"] = c.settings.MaxBytesToRead
|
||||
}
|
||||
|
||||
if c.settings.MaxExecutionTime != "" {
|
||||
settings["max_execution_time"] = c.settings.MaxExecutionTime
|
||||
}
|
||||
|
||||
if c.settings.MaxExecutionTimeLeaf != "" {
|
||||
settings["max_execution_time_leaf"] = c.settings.MaxExecutionTimeLeaf
|
||||
}
|
||||
|
||||
if c.settings.TimeoutBeforeCheckingExecutionSpeed != "" {
|
||||
settings["timeout_before_checking_execution_speed"] = c.settings.TimeoutBeforeCheckingExecutionSpeed
|
||||
}
|
||||
|
||||
// only list queries of
|
||||
if c.settings.OptimizeReadInOrderRegex != "" && c.settings.OptimizeReadInOrderRegexCompiled.Match([]byte(query)) {
|
||||
settings["optimize_read_in_order"] = 0
|
||||
}
|
||||
|
||||
ctx = clickhouse.Context(ctx, clickhouse.WithSettings(settings))
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (c clickhouseConnWrapper) getLogComment(ctx context.Context) string {
|
||||
// Get the key-value pairs from context for log comment
|
||||
kv := ctx.Value(common.LogCommentKey)
|
||||
if kv == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
logCommentKVs, ok := kv.(map[string]string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
logComment, _ := json.Marshal(logCommentKVs)
|
||||
|
||||
return string(logComment)
|
||||
}
|
||||
|
||||
func (c clickhouseConnWrapper) Query(ctx context.Context, query string, args ...interface{}) (driver.Rows, error) {
|
||||
return c.conn.Query(c.addClickHouseSettings(ctx, query), query, args...)
|
||||
}
|
||||
|
||||
func (c clickhouseConnWrapper) QueryRow(ctx context.Context, query string, args ...interface{}) driver.Row {
|
||||
return c.conn.QueryRow(c.addClickHouseSettings(ctx, query), query, args...)
|
||||
}
|
||||
|
||||
func (c clickhouseConnWrapper) Select(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
|
||||
return c.conn.Select(c.addClickHouseSettings(ctx, query), dest, query, args...)
|
||||
}
|
||||
|
||||
func (c clickhouseConnWrapper) Exec(ctx context.Context, query string, args ...interface{}) error {
|
||||
return c.conn.Exec(c.addClickHouseSettings(ctx, query), query, args...)
|
||||
}
|
||||
|
||||
func (c clickhouseConnWrapper) AsyncInsert(ctx context.Context, query string, wait bool, args ...interface{}) error {
|
||||
return c.conn.AsyncInsert(c.addClickHouseSettings(ctx, query), query, wait, args...)
|
||||
}
|
||||
|
||||
func (c clickhouseConnWrapper) PrepareBatch(ctx context.Context, query string, opts ...driver.PrepareBatchOption) (driver.Batch, error) {
|
||||
return c.conn.PrepareBatch(c.addClickHouseSettings(ctx, query), query, opts...)
|
||||
}
|
||||
|
||||
func (c clickhouseConnWrapper) ServerVersion() (*driver.ServerVersion, error) {
|
||||
return c.conn.ServerVersion()
|
||||
}
|
||||
|
||||
func (c clickhouseConnWrapper) Contributors() []string {
|
||||
return c.conn.Contributors()
|
||||
}
|
||||
@@ -97,10 +97,6 @@ type APIHandler struct {
|
||||
temporalityMap map[string]map[v3.Temporality]bool
|
||||
temporalityMux sync.Mutex
|
||||
|
||||
maxIdleConns int
|
||||
maxOpenConns int
|
||||
dialTimeout time.Duration
|
||||
|
||||
IntegrationsController *integrations.Controller
|
||||
|
||||
CloudIntegrationsController *cloudintegrations.Controller
|
||||
@@ -142,10 +138,6 @@ type APIHandlerOpts struct {
|
||||
|
||||
PreferSpanMetrics bool
|
||||
|
||||
MaxIdleConns int
|
||||
MaxOpenConns int
|
||||
DialTimeout time.Duration
|
||||
|
||||
// dao layer to perform crud on app objects like dashboard, alerts etc
|
||||
AppDao dao.ModelDao
|
||||
|
||||
@@ -225,9 +217,6 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) {
|
||||
skipConfig: opts.SkipConfig,
|
||||
preferSpanMetrics: opts.PreferSpanMetrics,
|
||||
temporalityMap: make(map[string]map[v3.Temporality]bool),
|
||||
maxIdleConns: opts.MaxIdleConns,
|
||||
maxOpenConns: opts.MaxOpenConns,
|
||||
dialTimeout: opts.DialTimeout,
|
||||
alertManager: alertManager,
|
||||
ruleManager: opts.RuleManager,
|
||||
featureFlags: opts.FeatureFlags,
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"go.signoz.io/signoz/pkg/query-service/model"
|
||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
"go.signoz.io/signoz/pkg/query-service/postprocess"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
@@ -332,23 +333,53 @@ func (h *HostsRepo) DidSendHostMetricsData(ctx context.Context, req model.HostLi
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (h *HostsRepo) IsSendingK8SAgentMetrics(ctx context.Context, req model.HostListRequest) (bool, error) {
|
||||
func (h *HostsRepo) IsSendingK8SAgentMetrics(ctx context.Context, req model.HostListRequest) ([]string, []string, error) {
|
||||
names := []string{}
|
||||
for _, metricName := range metricNamesForHosts {
|
||||
names = append(names, metricName)
|
||||
}
|
||||
namesStr := "'" + strings.Join(names, "','") + "'"
|
||||
|
||||
queryForRecentFingerprints := fmt.Sprintf(`
|
||||
SELECT DISTINCT fingerprint
|
||||
FROM %s.%s
|
||||
WHERE metric_name IN (%s)
|
||||
AND unix_milli >= toUnixTimestamp(now() - INTERVAL 5 MINUTE) * 1000`,
|
||||
constants.SIGNOZ_METRIC_DBNAME, constants.SIGNOZ_SAMPLES_V4_TABLENAME, namesStr)
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
SELECT count()
|
||||
SELECT DISTINCT JSONExtractString(labels, 'k8s_cluster_name') as k8s_cluster_name, JSONExtractString(labels, 'k8s_node_name') as k8s_node_name
|
||||
FROM %s.%s
|
||||
WHERE metric_name IN (%s)
|
||||
AND unix_milli >= toUnixTimestamp(now() - INTERVAL 60 MINUTE) * 1000
|
||||
AND JSONExtractString(labels, 'host_name') LIKE '%%-otel-agent%%'`,
|
||||
constants.SIGNOZ_METRIC_DBNAME, constants.SIGNOZ_TIMESERIES_V4_TABLENAME, namesStr)
|
||||
AND JSONExtractString(labels, 'host_name') LIKE '%%-otel-agent%%'
|
||||
AND fingerprint GLOBAL IN (%s)`,
|
||||
constants.SIGNOZ_METRIC_DBNAME, constants.SIGNOZ_TIMESERIES_V4_TABLENAME, namesStr, queryForRecentFingerprints)
|
||||
|
||||
count, err := h.reader.GetCountOfThings(ctx, query)
|
||||
return count > 0, err
|
||||
result, err := h.reader.GetListResultV3(ctx, query)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
clusterNames := make(map[string]struct{})
|
||||
nodeNames := make(map[string]struct{})
|
||||
|
||||
for _, row := range result {
|
||||
switch v := row.Data["k8s_cluster_name"].(type) {
|
||||
case string:
|
||||
clusterNames[v] = struct{}{}
|
||||
case *string:
|
||||
clusterNames[*v] = struct{}{}
|
||||
}
|
||||
switch v := row.Data["k8s_node_name"].(type) {
|
||||
case string:
|
||||
nodeNames[v] = struct{}{}
|
||||
case *string:
|
||||
nodeNames[*v] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
return maps.Keys(clusterNames), maps.Keys(nodeNames), nil
|
||||
}
|
||||
|
||||
func (h *HostsRepo) GetHostList(ctx context.Context, req model.HostListRequest) (model.HostListResponse, error) {
|
||||
@@ -372,8 +403,10 @@ func (h *HostsRepo) GetHostList(ctx context.Context, req model.HostListRequest)
|
||||
}
|
||||
|
||||
// don't fail the request if we can't get these values
|
||||
if sendingK8SAgentMetrics, err := h.IsSendingK8SAgentMetrics(ctx, req); err == nil {
|
||||
resp.IsSendingK8SAgentMetrics = sendingK8SAgentMetrics
|
||||
if clusterNames, nodeNames, err := h.IsSendingK8SAgentMetrics(ctx, req); err == nil {
|
||||
resp.IsSendingK8SAgentMetrics = len(clusterNames) > 0 || len(nodeNames) > 0
|
||||
resp.ClusterNames = clusterNames
|
||||
resp.NodeNames = nodeNames
|
||||
}
|
||||
if sentAnyHostMetricsData, err := h.DidSendHostMetricsData(ctx, req); err == nil {
|
||||
resp.SentAnyHostMetricsData = sentAnyHostMetricsData
|
||||
|
||||
@@ -1352,7 +1352,7 @@ func Test_querier_runWindowBasedListQuery(t *testing.T) {
|
||||
}
|
||||
testName := "name"
|
||||
|
||||
options := clickhouseReader.NewOptions("", 0, 0, 0, "", "archiveNamespace")
|
||||
options := clickhouseReader.NewOptions("", "", "archiveNamespace")
|
||||
|
||||
// iterate over test data, create reader and run test
|
||||
for _, tc := range testCases {
|
||||
|
||||
@@ -1406,7 +1406,7 @@ func Test_querier_runWindowBasedListQuery(t *testing.T) {
|
||||
}
|
||||
testName := "name"
|
||||
|
||||
options := clickhouseReader.NewOptions("", 0, 0, 0, "", "archiveNamespace")
|
||||
options := clickhouseReader.NewOptions("", "", "archiveNamespace")
|
||||
|
||||
// iterate over test data, create reader and run test
|
||||
for _, tc := range testCases {
|
||||
|
||||
@@ -62,9 +62,6 @@ type ServerOptions struct {
|
||||
DisableRules bool
|
||||
RuleRepoURL string
|
||||
PreferSpanMetrics bool
|
||||
MaxIdleConns int
|
||||
MaxOpenConns int
|
||||
DialTimeout time.Duration
|
||||
CacheConfigPath string
|
||||
FluxInterval string
|
||||
FluxIntervalForTraceDetail string
|
||||
@@ -132,11 +129,9 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
zap.L().Info("Using ClickHouse as datastore ...")
|
||||
clickhouseReader := clickhouseReader.NewReader(
|
||||
serverOptions.SigNoz.SQLStore.SQLxDB(),
|
||||
serverOptions.SigNoz.TelemetryStore.ClickHouseDB(),
|
||||
serverOptions.PromConfigPath,
|
||||
fm,
|
||||
serverOptions.MaxIdleConns,
|
||||
serverOptions.MaxOpenConns,
|
||||
serverOptions.DialTimeout,
|
||||
serverOptions.Cluster,
|
||||
serverOptions.UseLogsNewSchema,
|
||||
serverOptions.UseTraceNewSchema,
|
||||
@@ -202,9 +197,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
Reader: reader,
|
||||
SkipConfig: skipConfig,
|
||||
PreferSpanMetrics: serverOptions.PreferSpanMetrics,
|
||||
MaxIdleConns: serverOptions.MaxIdleConns,
|
||||
MaxOpenConns: serverOptions.MaxOpenConns,
|
||||
DialTimeout: serverOptions.DialTimeout,
|
||||
AppDao: dao.DB(),
|
||||
RuleManager: rm,
|
||||
FeatureFlags: fm,
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/ClickHouse/clickhouse-go/v2"
|
||||
"github.com/prometheus/prometheus/promql"
|
||||
"github.com/prometheus/prometheus/storage"
|
||||
"github.com/prometheus/prometheus/util/stats"
|
||||
@@ -85,7 +84,6 @@ type Reader interface {
|
||||
) (*v3.QBFilterSuggestionsResponse, *model.ApiError)
|
||||
|
||||
// Connection needed for rules, not ideal but required
|
||||
GetConn() clickhouse.Conn
|
||||
GetQueryEngine() *promql.Engine
|
||||
GetFanoutStorage() *storage.Storage
|
||||
|
||||
|
||||
@@ -85,6 +85,10 @@ func main() {
|
||||
envprovider.NewFactory(),
|
||||
fileprovider.NewFactory(),
|
||||
},
|
||||
}, signoz.DeprecatedFlags{
|
||||
MaxIdleConns: maxIdleConns,
|
||||
MaxOpenConns: maxOpenConns,
|
||||
DialTimeout: dialTimeout,
|
||||
})
|
||||
if err != nil {
|
||||
zap.L().Fatal("Failed to create config", zap.Error(err))
|
||||
@@ -104,9 +108,6 @@ func main() {
|
||||
PrivateHostPort: constants.PrivateHostPort,
|
||||
DisableRules: disableRules,
|
||||
RuleRepoURL: ruleRepoURL,
|
||||
MaxIdleConns: maxIdleConns,
|
||||
MaxOpenConns: maxOpenConns,
|
||||
DialTimeout: dialTimeout,
|
||||
CacheConfigPath: cacheConfigPath,
|
||||
FluxInterval: fluxInterval,
|
||||
FluxIntervalForTraceDetail: fluxIntervalForTraceDetail,
|
||||
|
||||
@@ -42,6 +42,8 @@ type HostListResponse struct {
|
||||
Total int `json:"total"`
|
||||
SentAnyHostMetricsData bool `json:"sentAnyHostMetricsData"`
|
||||
IsSendingK8SAgentMetrics bool `json:"isSendingK8SAgentMetrics"`
|
||||
ClusterNames []string `json:"clusterNames"`
|
||||
NodeNames []string `json:"nodeNames"`
|
||||
}
|
||||
|
||||
func (r *HostListResponse) SortBy(orderBy *v3.OrderBy) {
|
||||
|
||||
@@ -1240,7 +1240,7 @@ func TestThresholdRuleUnitCombinations(t *testing.T) {
|
||||
"summary": "The rule threshold is set to {{$threshold}}, and the observed metric value is {{$value}}",
|
||||
}
|
||||
|
||||
options := clickhouseReader.NewOptions("", 0, 0, 0, "", "archiveNamespace")
|
||||
options := clickhouseReader.NewOptions("", "", "archiveNamespace")
|
||||
reader := clickhouseReader.NewReaderFromClickhouseConnection(mock, options, nil, "", fm, "", true, true, time.Duration(time.Second), nil)
|
||||
|
||||
rule, err := NewThresholdRule("69", &postableRule, fm, reader, true, true)
|
||||
@@ -1339,7 +1339,7 @@ func TestThresholdRuleNoData(t *testing.T) {
|
||||
"summary": "The rule threshold is set to {{$threshold}}, and the observed metric value is {{$value}}",
|
||||
}
|
||||
|
||||
options := clickhouseReader.NewOptions("", 0, 0, 0, "", "archiveNamespace")
|
||||
options := clickhouseReader.NewOptions("", "", "archiveNamespace")
|
||||
reader := clickhouseReader.NewReaderFromClickhouseConnection(mock, options, nil, "", fm, "", true, true, time.Duration(time.Second), nil)
|
||||
|
||||
rule, err := NewThresholdRule("69", &postableRule, fm, reader, true, true)
|
||||
@@ -1447,7 +1447,7 @@ func TestThresholdRuleTracesLink(t *testing.T) {
|
||||
"summary": "The rule threshold is set to {{$threshold}}, and the observed metric value is {{$value}}",
|
||||
}
|
||||
|
||||
options := clickhouseReader.NewOptions("", 0, 0, 0, "", "archiveNamespace")
|
||||
options := clickhouseReader.NewOptions("", "", "archiveNamespace")
|
||||
reader := clickhouseReader.NewReaderFromClickhouseConnection(mock, options, nil, "", fm, "", true, true, time.Duration(time.Second), nil)
|
||||
|
||||
rule, err := NewThresholdRule("69", &postableRule, fm, reader, true, true)
|
||||
@@ -1572,7 +1572,7 @@ func TestThresholdRuleLogsLink(t *testing.T) {
|
||||
"summary": "The rule threshold is set to {{$threshold}}, and the observed metric value is {{$value}}",
|
||||
}
|
||||
|
||||
options := clickhouseReader.NewOptions("", 0, 0, 0, "", "archiveNamespace")
|
||||
options := clickhouseReader.NewOptions("", "", "archiveNamespace")
|
||||
reader := clickhouseReader.NewReaderFromClickhouseConnection(mock, options, nil, "", fm, "", true, true, time.Duration(time.Second), nil)
|
||||
|
||||
rule, err := NewThresholdRule("69", &postableRule, fm, reader, true, true)
|
||||
|
||||
@@ -40,7 +40,7 @@ func NewMockClickhouseReader(
|
||||
require.Nil(t, err, "could not init mock clickhouse")
|
||||
reader := clickhouseReader.NewReaderFromClickhouseConnection(
|
||||
mockDB,
|
||||
clickhouseReader.NewOptions("", 10, 10, 10*time.Second, ""),
|
||||
clickhouseReader.NewOptions("", ""),
|
||||
testDB,
|
||||
"",
|
||||
featureFlags,
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"go.signoz.io/signoz/pkg/instrumentation"
|
||||
"go.signoz.io/signoz/pkg/sqlmigrator"
|
||||
"go.signoz.io/signoz/pkg/sqlstore"
|
||||
"go.signoz.io/signoz/pkg/telemetrystore"
|
||||
"go.signoz.io/signoz/pkg/web"
|
||||
)
|
||||
|
||||
@@ -35,9 +36,20 @@ type Config struct {
|
||||
|
||||
// API Server config
|
||||
APIServer apiserver.Config `mapstructure:"apiserver"`
|
||||
|
||||
// TelemetryStore config
|
||||
TelemetryStore telemetrystore.Config `mapstructure:"telemetrystore"`
|
||||
}
|
||||
|
||||
func NewConfig(ctx context.Context, resolverConfig config.ResolverConfig) (Config, error) {
|
||||
// DeprecatedFlags are the flags that are deprecated and scheduled for removal.
|
||||
// These flags are used to ensure backward compatibility with the old flags.
|
||||
type DeprecatedFlags struct {
|
||||
MaxIdleConns int
|
||||
MaxOpenConns int
|
||||
DialTimeout time.Duration
|
||||
}
|
||||
|
||||
func NewConfig(ctx context.Context, resolverConfig config.ResolverConfig, deprecatedFlags DeprecatedFlags) (Config, error) {
|
||||
configFactories := []factory.ConfigFactory{
|
||||
instrumentation.NewConfigFactory(),
|
||||
web.NewConfigFactory(),
|
||||
@@ -45,6 +57,7 @@ func NewConfig(ctx context.Context, resolverConfig config.ResolverConfig) (Confi
|
||||
sqlstore.NewConfigFactory(),
|
||||
sqlmigrator.NewConfigFactory(),
|
||||
apiserver.NewConfigFactory(),
|
||||
telemetrystore.NewConfigFactory(),
|
||||
}
|
||||
|
||||
conf, err := config.New(ctx, resolverConfig, configFactories)
|
||||
@@ -57,12 +70,12 @@ func NewConfig(ctx context.Context, resolverConfig config.ResolverConfig) (Confi
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
mergeAndEnsureBackwardCompatibility(&config)
|
||||
mergeAndEnsureBackwardCompatibility(&config, deprecatedFlags)
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func mergeAndEnsureBackwardCompatibility(config *Config) {
|
||||
func mergeAndEnsureBackwardCompatibility(config *Config, deprecatedFlags DeprecatedFlags) {
|
||||
// SIGNOZ_LOCAL_DB_PATH
|
||||
if os.Getenv("SIGNOZ_LOCAL_DB_PATH") != "" {
|
||||
fmt.Println("[Deprecated] env SIGNOZ_LOCAL_DB_PATH is deprecated and scheduled for removal. Please use SIGNOZ_SQLSTORE_SQLITE_PATH instead.")
|
||||
@@ -87,4 +100,21 @@ func mergeAndEnsureBackwardCompatibility(config *Config) {
|
||||
fmt.Println("Error parsing CONTEXT_TIMEOUT_MAX_ALLOWED, using default value of 600s")
|
||||
}
|
||||
}
|
||||
if os.Getenv("ClickHouseUrl") != "" {
|
||||
fmt.Println("[Deprecated] env ClickHouseUrl is deprecated and scheduled for removal. Please use SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN instead.")
|
||||
config.TelemetryStore.ClickHouse.DSN = os.Getenv("ClickHouseUrl")
|
||||
}
|
||||
|
||||
if deprecatedFlags.MaxIdleConns != 50 {
|
||||
fmt.Println("[Deprecated] flag --max-idle-conns is deprecated and scheduled for removal. Please use SIGNOZ_TELEMETRYSTORE_MAX__IDLE__CONNS env variable instead.")
|
||||
config.TelemetryStore.Connection.MaxIdleConns = deprecatedFlags.MaxIdleConns
|
||||
}
|
||||
if deprecatedFlags.MaxOpenConns != 100 {
|
||||
fmt.Println("[Deprecated] flag --max-open-conns is deprecated and scheduled for removal. Please use SIGNOZ_TELEMETRYSTORE_MAX__OPEN__CONNS env variable instead.")
|
||||
config.TelemetryStore.Connection.MaxOpenConns = deprecatedFlags.MaxOpenConns
|
||||
}
|
||||
if deprecatedFlags.DialTimeout != 5*time.Second {
|
||||
fmt.Println("[Deprecated] flag --dial-timeout is deprecated and scheduled for removal. Please use SIGNOZ_TELEMETRYSTORE_DIAL__TIMEOUT environment variable instead.")
|
||||
config.TelemetryStore.Connection.DialTimeout = deprecatedFlags.DialTimeout
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ import (
|
||||
"go.signoz.io/signoz/pkg/sqlmigration"
|
||||
"go.signoz.io/signoz/pkg/sqlstore"
|
||||
"go.signoz.io/signoz/pkg/sqlstore/sqlitesqlstore"
|
||||
"go.signoz.io/signoz/pkg/telemetrystore"
|
||||
"go.signoz.io/signoz/pkg/telemetrystore/clickhousetelemetrystore"
|
||||
"go.signoz.io/signoz/pkg/telemetrystore/telemetrystorehook"
|
||||
"go.signoz.io/signoz/pkg/web"
|
||||
"go.signoz.io/signoz/pkg/web/noopweb"
|
||||
"go.signoz.io/signoz/pkg/web/routerweb"
|
||||
@@ -25,9 +28,13 @@ type ProviderConfig struct {
|
||||
|
||||
// Map of all sql migration provider factories
|
||||
SQLMigrationProviderFactories factory.NamedMap[factory.ProviderFactory[sqlmigration.SQLMigration, sqlmigration.Config]]
|
||||
|
||||
// Map of all telemetrystore provider factories
|
||||
TelemetryStoreProviderFactories factory.NamedMap[factory.ProviderFactory[telemetrystore.TelemetryStore, telemetrystore.Config]]
|
||||
}
|
||||
|
||||
func NewProviderConfig() ProviderConfig {
|
||||
hook := telemetrystorehook.NewFactory()
|
||||
return ProviderConfig{
|
||||
CacheProviderFactories: factory.MustNewNamedMap(
|
||||
memorycache.NewFactory(),
|
||||
@@ -50,5 +57,8 @@ func NewProviderConfig() ProviderConfig {
|
||||
sqlmigration.NewAddPipelinesFactory(),
|
||||
sqlmigration.NewAddIntegrationsFactory(),
|
||||
),
|
||||
TelemetryStoreProviderFactories: factory.MustNewNamedMap(
|
||||
clickhousetelemetrystore.NewFactory(hook),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,17 @@ import (
|
||||
"go.signoz.io/signoz/pkg/factory"
|
||||
"go.signoz.io/signoz/pkg/instrumentation"
|
||||
"go.signoz.io/signoz/pkg/sqlstore"
|
||||
"go.signoz.io/signoz/pkg/telemetrystore"
|
||||
"go.signoz.io/signoz/pkg/version"
|
||||
|
||||
"go.signoz.io/signoz/pkg/web"
|
||||
)
|
||||
|
||||
type SigNoz struct {
|
||||
Cache cache.Cache
|
||||
Web web.Web
|
||||
SQLStore sqlstore.SQLStore
|
||||
Cache cache.Cache
|
||||
Web web.Web
|
||||
SQLStore sqlstore.SQLStore
|
||||
TelemetryStore telemetrystore.TelemetryStore
|
||||
}
|
||||
|
||||
func New(
|
||||
@@ -68,9 +70,21 @@ func New(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
telemetrystore, err := factory.NewProviderFromNamedMap(
|
||||
ctx,
|
||||
providerSettings,
|
||||
config.TelemetryStore,
|
||||
providerConfig.TelemetryStoreProviderFactories,
|
||||
config.TelemetryStore.Provider,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &SigNoz{
|
||||
Cache: cache,
|
||||
Web: web,
|
||||
SQLStore: sqlstore,
|
||||
Cache: cache,
|
||||
Web: web,
|
||||
SQLStore: sqlstore,
|
||||
TelemetryStore: telemetrystore,
|
||||
}, nil
|
||||
}
|
||||
|
||||
120
pkg/telemetrystore/clickhousetelemetrystore/provider.go
Normal file
120
pkg/telemetrystore/clickhousetelemetrystore/provider.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package clickhousetelemetrystore
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/ClickHouse/clickhouse-go/v2"
|
||||
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
|
||||
"go.signoz.io/signoz/pkg/factory"
|
||||
"go.signoz.io/signoz/pkg/telemetrystore"
|
||||
)
|
||||
|
||||
type provider struct {
|
||||
settings factory.ScopedProviderSettings
|
||||
clickHouseConn clickhouse.Conn
|
||||
hooks []telemetrystore.TelemetryStoreHook
|
||||
}
|
||||
|
||||
func NewFactory(hookFactories ...factory.ProviderFactory[telemetrystore.TelemetryStoreHook, telemetrystore.Config]) factory.ProviderFactory[telemetrystore.TelemetryStore, telemetrystore.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("clickhouse"), func(ctx context.Context, providerSettings factory.ProviderSettings, config telemetrystore.Config) (telemetrystore.TelemetryStore, error) {
|
||||
// we want to fail fast so we have hook registration errors before creating the telemetry store
|
||||
hooks := make([]telemetrystore.TelemetryStoreHook, len(hookFactories))
|
||||
for i, hookFactory := range hookFactories {
|
||||
hook, err := hookFactory.New(ctx, providerSettings, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hooks[i] = hook
|
||||
}
|
||||
return New(ctx, providerSettings, config, hooks...)
|
||||
})
|
||||
}
|
||||
|
||||
func New(ctx context.Context, providerSettings factory.ProviderSettings, config telemetrystore.Config, hooks ...telemetrystore.TelemetryStoreHook) (telemetrystore.TelemetryStore, error) {
|
||||
settings := factory.NewScopedProviderSettings(providerSettings, "go.signoz.io/signoz/pkg/telemetrystore/clickhousetelemetrystore")
|
||||
|
||||
options, err := clickhouse.ParseDSN(config.ClickHouse.DSN)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
options.MaxIdleConns = config.Connection.MaxIdleConns
|
||||
options.MaxOpenConns = config.Connection.MaxOpenConns
|
||||
options.DialTimeout = config.Connection.DialTimeout
|
||||
|
||||
chConn, err := clickhouse.Open(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &provider{
|
||||
settings: settings,
|
||||
clickHouseConn: chConn,
|
||||
hooks: hooks,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *provider) ClickHouseDB() clickhouse.Conn {
|
||||
return p
|
||||
}
|
||||
|
||||
func (p provider) Close() error {
|
||||
return p.clickHouseConn.Close()
|
||||
}
|
||||
|
||||
func (p provider) Ping(ctx context.Context) error {
|
||||
return p.clickHouseConn.Ping(ctx)
|
||||
}
|
||||
|
||||
func (p provider) Stats() driver.Stats {
|
||||
return p.clickHouseConn.Stats()
|
||||
}
|
||||
|
||||
func (p provider) Query(ctx context.Context, query string, args ...interface{}) (driver.Rows, error) {
|
||||
ctx, query, args = telemetrystore.WrapBeforeQuery(p.hooks, ctx, query, args...)
|
||||
rows, err := p.clickHouseConn.Query(ctx, query, args...)
|
||||
telemetrystore.WrapAfterQuery(p.hooks, ctx, query, args, rows, err)
|
||||
return rows, err
|
||||
}
|
||||
|
||||
func (p provider) QueryRow(ctx context.Context, query string, args ...interface{}) driver.Row {
|
||||
ctx, query, args = telemetrystore.WrapBeforeQuery(p.hooks, ctx, query, args...)
|
||||
row := p.clickHouseConn.QueryRow(ctx, query, args...)
|
||||
telemetrystore.WrapAfterQuery(p.hooks, ctx, query, args, nil, nil)
|
||||
return row
|
||||
}
|
||||
|
||||
func (p provider) Select(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
|
||||
ctx, query, args = telemetrystore.WrapBeforeQuery(p.hooks, ctx, query, args...)
|
||||
err := p.clickHouseConn.Select(ctx, dest, query, args...)
|
||||
telemetrystore.WrapAfterQuery(p.hooks, ctx, query, args, nil, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p provider) Exec(ctx context.Context, query string, args ...interface{}) error {
|
||||
ctx, query, args = telemetrystore.WrapBeforeQuery(p.hooks, ctx, query, args...)
|
||||
err := p.clickHouseConn.Exec(ctx, query, args...)
|
||||
telemetrystore.WrapAfterQuery(p.hooks, ctx, query, args, nil, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p provider) AsyncInsert(ctx context.Context, query string, wait bool, args ...interface{}) error {
|
||||
ctx, query, args = telemetrystore.WrapBeforeQuery(p.hooks, ctx, query, args...)
|
||||
err := p.clickHouseConn.AsyncInsert(ctx, query, wait, args...)
|
||||
telemetrystore.WrapAfterQuery(p.hooks, ctx, query, args, nil, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p provider) PrepareBatch(ctx context.Context, query string, opts ...driver.PrepareBatchOption) (driver.Batch, error) {
|
||||
ctx, query, args := telemetrystore.WrapBeforeQuery(p.hooks, ctx, query)
|
||||
batch, err := p.clickHouseConn.PrepareBatch(ctx, query, opts...)
|
||||
telemetrystore.WrapAfterQuery(p.hooks, ctx, query, args, nil, err)
|
||||
return batch, err
|
||||
}
|
||||
|
||||
func (p provider) ServerVersion() (*driver.ServerVersion, error) {
|
||||
return p.clickHouseConn.ServerVersion()
|
||||
}
|
||||
|
||||
func (p provider) Contributors() []string {
|
||||
return p.clickHouseConn.Contributors()
|
||||
}
|
||||
62
pkg/telemetrystore/config.go
Normal file
62
pkg/telemetrystore/config.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package telemetrystore
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.signoz.io/signoz/pkg/factory"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
// Provider is the provider to use
|
||||
Provider string `mapstructure:"provider"`
|
||||
// Connection is the connection configuration
|
||||
Connection ConnectionConfig `mapstructure:",squash"`
|
||||
// Clickhouse is the clickhouse configuration
|
||||
ClickHouse ClickHouseConfig `mapstructure:"clickhouse"`
|
||||
}
|
||||
|
||||
type ConnectionConfig struct {
|
||||
// MaxOpenConns is the maximum number of open connections to the database.
|
||||
MaxOpenConns int `mapstructure:"max_open_conns"`
|
||||
MaxIdleConns int `mapstructure:"max_idle_conns"`
|
||||
DialTimeout time.Duration `mapstructure:"dial_timeout"`
|
||||
}
|
||||
|
||||
type ClickHouseQuerySettings struct {
|
||||
MaxExecutionTime int `mapstructure:"max_execution_time"`
|
||||
MaxExecutionTimeLeaf int `mapstructure:"max_execution_time_leaf"`
|
||||
TimeoutBeforeCheckingExecutionSpeed int `mapstructure:"timeout_before_checking_execution_speed"`
|
||||
MaxBytesToRead int `mapstructure:"max_bytes_to_read"`
|
||||
MaxResultRowsForCHQuery int `mapstructure:"max_result_rows_for_ch_query"`
|
||||
}
|
||||
|
||||
type ClickHouseConfig struct {
|
||||
DSN string `mapstructure:"dsn"`
|
||||
|
||||
QuerySettings ClickHouseQuerySettings `mapstructure:"settings"`
|
||||
}
|
||||
|
||||
func NewConfigFactory() factory.ConfigFactory {
|
||||
return factory.NewConfigFactory(factory.MustNewName("telemetrystore"), newConfig)
|
||||
|
||||
}
|
||||
|
||||
func newConfig() factory.Config {
|
||||
return Config{
|
||||
Provider: "clickhouse",
|
||||
Connection: ConnectionConfig{
|
||||
MaxOpenConns: 100,
|
||||
MaxIdleConns: 50,
|
||||
DialTimeout: 5 * time.Second,
|
||||
},
|
||||
ClickHouse: ClickHouseConfig{
|
||||
DSN: "http://localhost:9000",
|
||||
|
||||
// No default query settings, as default's are set in ch config
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c Config) Validate() error {
|
||||
return nil
|
||||
}
|
||||
95
pkg/telemetrystore/config_test.go
Normal file
95
pkg/telemetrystore/config_test.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package telemetrystore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.signoz.io/signoz/pkg/config"
|
||||
"go.signoz.io/signoz/pkg/config/envprovider"
|
||||
"go.signoz.io/signoz/pkg/factory"
|
||||
)
|
||||
|
||||
func TestNewWithEnvProvider(t *testing.T) {
|
||||
t.Setenv("SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN", "http://localhost:9000")
|
||||
t.Setenv("SIGNOZ_TELEMETRYSTORE_MAX__IDLE__CONNS", "60")
|
||||
t.Setenv("SIGNOZ_TELEMETRYSTORE_MAX__OPEN__CONNS", "150")
|
||||
t.Setenv("SIGNOZ_TELEMETRYSTORE_DIAL__TIMEOUT", "5s")
|
||||
t.Setenv("SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DEBUG", "true")
|
||||
|
||||
conf, err := config.New(
|
||||
context.Background(),
|
||||
config.ResolverConfig{
|
||||
Uris: []string{"env:"},
|
||||
ProviderFactories: []config.ProviderFactory{
|
||||
envprovider.NewFactory(),
|
||||
},
|
||||
},
|
||||
[]factory.ConfigFactory{
|
||||
NewConfigFactory(),
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
actual := &Config{}
|
||||
err = conf.Unmarshal("telemetrystore", actual)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := &Config{
|
||||
Provider: "clickhouse",
|
||||
Connection: ConnectionConfig{
|
||||
MaxOpenConns: 150,
|
||||
MaxIdleConns: 60,
|
||||
DialTimeout: 5 * time.Second,
|
||||
},
|
||||
ClickHouse: ClickHouseConfig{
|
||||
DSN: "http://localhost:9000",
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestNewWithEnvProviderWithQuerySettings(t *testing.T) {
|
||||
t.Setenv("SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_SETTINGS_MAX__EXECUTION__TIME", "10")
|
||||
t.Setenv("SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_SETTINGS_MAX__EXECUTION__TIME__LEAF", "10")
|
||||
t.Setenv("SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_SETTINGS_TIMEOUT__BEFORE__CHECKING__EXECUTION__SPEED", "10")
|
||||
t.Setenv("SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_SETTINGS_MAX__BYTES__TO__READ", "1000000")
|
||||
t.Setenv("SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_SETTINGS_MAX__RESULT__ROWS__FOR__CH__QUERY", "10000")
|
||||
|
||||
conf, err := config.New(
|
||||
context.Background(),
|
||||
config.ResolverConfig{
|
||||
Uris: []string{"env:"},
|
||||
ProviderFactories: []config.ProviderFactory{
|
||||
envprovider.NewFactory(),
|
||||
},
|
||||
},
|
||||
[]factory.ConfigFactory{
|
||||
NewConfigFactory(),
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
actual := &Config{}
|
||||
err = conf.Unmarshal("telemetrystore", actual)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := &Config{
|
||||
ClickHouse: ClickHouseConfig{
|
||||
QuerySettings: ClickHouseQuerySettings{
|
||||
MaxExecutionTime: 10,
|
||||
MaxExecutionTimeLeaf: 10,
|
||||
TimeoutBeforeCheckingExecutionSpeed: 10,
|
||||
MaxBytesToRead: 1000000,
|
||||
MaxResultRowsForCHQuery: 10000,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expected.ClickHouse.QuerySettings, actual.ClickHouse.QuerySettings)
|
||||
}
|
||||
32
pkg/telemetrystore/telemetrystore.go
Normal file
32
pkg/telemetrystore/telemetrystore.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package telemetrystore
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/ClickHouse/clickhouse-go/v2"
|
||||
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
|
||||
)
|
||||
|
||||
type TelemetryStore interface {
|
||||
// Returns the SigNoz Wrapper for Clickhouse
|
||||
ClickHouseDB() clickhouse.Conn
|
||||
}
|
||||
|
||||
type TelemetryStoreHook interface {
|
||||
BeforeQuery(ctx context.Context, query string, args ...interface{}) (context.Context, string, []interface{})
|
||||
AfterQuery(ctx context.Context, query string, args []interface{}, rows driver.Rows, err error)
|
||||
}
|
||||
|
||||
func WrapBeforeQuery(hooks []TelemetryStoreHook, ctx context.Context, query string, args ...interface{}) (context.Context, string, []interface{}) {
|
||||
for _, hook := range hooks {
|
||||
ctx, query, args = hook.BeforeQuery(ctx, query, args...)
|
||||
}
|
||||
return ctx, query, args
|
||||
}
|
||||
|
||||
// runAfterHooks executes all after hooks in order
|
||||
func WrapAfterQuery(hooks []TelemetryStoreHook, ctx context.Context, query string, args []interface{}, rows driver.Rows, err error) {
|
||||
for _, hook := range hooks {
|
||||
hook.AfterQuery(ctx, query, args, rows, err)
|
||||
}
|
||||
}
|
||||
85
pkg/telemetrystore/telemetrystorehook/settings.go
Normal file
85
pkg/telemetrystore/telemetrystorehook/settings.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package telemetrystorehook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/ClickHouse/clickhouse-go/v2"
|
||||
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
|
||||
"go.signoz.io/signoz/pkg/factory"
|
||||
"go.signoz.io/signoz/pkg/query-service/common"
|
||||
"go.signoz.io/signoz/pkg/telemetrystore"
|
||||
)
|
||||
|
||||
type provider struct {
|
||||
settings telemetrystore.ClickHouseQuerySettings
|
||||
}
|
||||
|
||||
func NewFactory() factory.ProviderFactory[telemetrystore.TelemetryStoreHook, telemetrystore.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("clickhousesettings"), New)
|
||||
}
|
||||
|
||||
func New(ctx context.Context, providerSettings factory.ProviderSettings, config telemetrystore.Config) (telemetrystore.TelemetryStoreHook, error) {
|
||||
return &provider{
|
||||
settings: config.ClickHouse.QuerySettings,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *provider) BeforeQuery(ctx context.Context, query string, args ...interface{}) (context.Context, string, []interface{}) {
|
||||
return h.clickHouseSettings(ctx, query, args...)
|
||||
}
|
||||
|
||||
func (h *provider) AfterQuery(ctx context.Context, query string, args []interface{}, rows driver.Rows, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// clickHouseSettings adds clickhouse settings to queries
|
||||
func (h *provider) clickHouseSettings(ctx context.Context, query string, args ...interface{}) (context.Context, string, []interface{}) {
|
||||
settings := clickhouse.Settings{}
|
||||
|
||||
// Apply default settings
|
||||
logComment := h.getLogComment(ctx)
|
||||
if logComment != "" {
|
||||
settings["log_comment"] = logComment
|
||||
}
|
||||
|
||||
if ctx.Value("enforce_max_result_rows") != nil {
|
||||
settings["max_result_rows"] = h.settings.MaxResultRowsForCHQuery
|
||||
}
|
||||
|
||||
if h.settings.MaxBytesToRead != 0 {
|
||||
settings["max_bytes_to_read"] = h.settings.MaxBytesToRead
|
||||
}
|
||||
|
||||
if h.settings.MaxExecutionTime != 0 {
|
||||
settings["max_execution_time"] = h.settings.MaxExecutionTime
|
||||
}
|
||||
|
||||
if h.settings.MaxExecutionTimeLeaf != 0 {
|
||||
settings["max_execution_time_leaf"] = h.settings.MaxExecutionTimeLeaf
|
||||
}
|
||||
|
||||
if h.settings.TimeoutBeforeCheckingExecutionSpeed != 0 {
|
||||
settings["timeout_before_checking_execution_speed"] = h.settings.TimeoutBeforeCheckingExecutionSpeed
|
||||
}
|
||||
|
||||
ctx = clickhouse.Context(ctx, clickhouse.WithSettings(settings))
|
||||
return ctx, query, args
|
||||
}
|
||||
|
||||
func (h *provider) getLogComment(ctx context.Context) string {
|
||||
// Get the key-value pairs from context for log comment
|
||||
kv := ctx.Value(common.LogCommentKey)
|
||||
if kv == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
logCommentKVs, ok := kv.(map[string]string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
logComment, _ := json.Marshal(logCommentKVs)
|
||||
|
||||
return string(logComment)
|
||||
}
|
||||
34
pkg/telemetrystore/telemetrystoretest/provider.go
Normal file
34
pkg/telemetrystore/telemetrystoretest/provider.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package telemetrystoretest
|
||||
|
||||
import (
|
||||
"github.com/ClickHouse/clickhouse-go/v2"
|
||||
cmock "github.com/srikanthccv/ClickHouse-go-mock"
|
||||
)
|
||||
|
||||
// Provider represents a mock telemetry store provider for testing
|
||||
type Provider struct {
|
||||
mock cmock.ClickConnMockCommon
|
||||
}
|
||||
|
||||
// New creates a new mock telemetry store provider
|
||||
func New() (*Provider, error) {
|
||||
options := &clickhouse.Options{} // Default options
|
||||
mock, err := cmock.NewClickHouseNative(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Provider{
|
||||
mock: mock,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Clickhouse returns the mock Clickhouse connection
|
||||
func (p *Provider) Clickhouse() clickhouse.Conn {
|
||||
return p.mock.(clickhouse.Conn)
|
||||
}
|
||||
|
||||
// Mock returns the underlying Clickhouse mock instance for setting expectations
|
||||
func (p *Provider) Mock() cmock.ClickConnMockCommon {
|
||||
return p.mock
|
||||
}
|
||||
44
pkg/telemetrystore/telemetrystoretest/provider_test.go
Normal file
44
pkg/telemetrystore/telemetrystoretest/provider_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package telemetrystoretest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ClickHouse/clickhouse-go/v2"
|
||||
cmock "github.com/srikanthccv/ClickHouse-go-mock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "should create new provider successfully",
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
provider, err := New()
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, provider)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, provider)
|
||||
assert.NotNil(t, provider.Mock())
|
||||
assert.NotNil(t, provider.Clickhouse())
|
||||
|
||||
// Verify the returned interfaces implement the expected types
|
||||
_, ok := provider.Mock().(cmock.ClickConnMockCommon)
|
||||
assert.True(t, ok, "Mock() should return cmock.ClickConnMockCommon")
|
||||
|
||||
_, ok = provider.Clickhouse().(clickhouse.Conn)
|
||||
assert.True(t, ok, "Clickhouse() should return clickhouse.Conn")
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user