Compare commits

...

12 Commits

Author SHA1 Message Date
Prashant Shahi
abea45f255 Merge branch 'main' into fix/install-no-sudo 2025-01-30 18:47:43 +05:45
Nityananda Gohain
d1e7cc128f fix: telemetry store (#6923)
* fix: inital changes for telemetry store

* fix: add tests and use proper config for conn

* fix: add telemetry store test

* fix: add backward compatibility for old variables and update example conf

* fix: move wrapper to telemetry store

* fix: no need to pass query for settings

* fix: remove redundant config for ch conn

* fix: use clickhouse dsn instead

* fix: update example config

* fix: update backward compatibility code

* fix: use hooks in telemetrystore

* fix: address minor comments

---------

Co-authored-by: Vibhu Pandey <vibhupandey28@gmail.com>
2025-01-30 10:21:55 +00:00
Amlan Kumar Nandy
ffd72cf406 chore: fix visibility toggle in host filters (#6976) 2025-01-30 00:15:19 +05:30
Vikrant Gupta
6dfea14219 chore(license): default to basic plan if license validation fails (#6972)
* chore(license): default to basic plan if license validation fails 3 times

* chore(license): revert the on-boot validation check

* chore(license): reset the atomic counter

* chore(license): revert the table creation removal

* chore(license): remove verify issue workflow

* chore(license): add proper log level

* chore(license): add proper log level

* chore(license): close the validation go routine post defaulting to basic plan

* chore(license): set the validator running flag to false as well
2025-01-29 18:17:50 +00:00
Shaheer Kochai
f2be856f63 fix: apply x axis date/time formatting only to panel types that have date/time in x axis (#6956) 2025-01-29 18:09:04 +00:00
Shaheer Kochai
f04589a0b2 fix: properly get the alert severity (#6974) 2025-01-29 17:30:51 +00:00
Amlan Kumar Nandy
1378590429 chore: fix filter editing issue in infra monitoring (#6961) 2025-01-29 17:21:57 +00:00
Srikanth Chekuri
88084af4d4 chore: add cluster, nodes that are sending incorrect host metrics (#6969) 2025-01-29 14:15:48 +00:00
Amlan Kumar Nandy
d0eefa0cf2 feat: add hostname and os type quick filter to hosts list (#6926) 2025-01-29 13:47:23 +00:00
Vikrant Gupta
cc9eb32c50 chore(license): clean up the older endpoints (#6971) 2025-01-29 18:13:50 +05:30
Prashant Shahi
c0d8f8de3a Merge branch 'main' into fix/install-no-sudo 2025-01-27 11:35:23 +05:45
Prashant Shahi
860fa4a995 fix(install-script): handle no sudo command scenerio
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2025-01-27 01:59:35 +05:30
49 changed files with 1138 additions and 764 deletions

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,
}
}

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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")
}

View File

@@ -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,

View File

@@ -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"`

View File

@@ -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 {

View File

@@ -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';

View File

@@ -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';

View File

@@ -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),
};

View File

@@ -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(
() => ({

View File

@@ -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

View 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',
})}
/>
);
}

View File

@@ -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;

View File

@@ -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,
},
];

View File

@@ -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}>

View File

@@ -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>

View File

@@ -387,6 +387,6 @@ export const getUPlotChartOptions = ({
hiddenGraph,
isDarkMode,
}),
axes: getAxes(isDarkMode, yAxisUnit),
axes: getAxes({ isDarkMode, yAxisUnit, panelType }),
};
};

View File

@@ -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);

View File

@@ -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,
},
{

View File

@@ -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

View File

@@ -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}

View File

@@ -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 {

View File

@@ -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()
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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,

View File

@@ -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
}
}

View File

@@ -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),
),
}
}

View File

@@ -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
}

View 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()
}

View 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
}

View 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)
}

View 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)
}
}

View 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)
}

View 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
}

View 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")
})
}
}