Merge pull request #6546 from SigNoz/release/v0.60.x

Release/v0.60.x
This commit is contained in:
Prashant Shahi 2024-11-27 15:54:49 +05:30 committed by GitHub
commit a320a16556
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
138 changed files with 4941 additions and 1852 deletions

View File

@ -146,7 +146,7 @@ services:
condition: on-failure condition: on-failure
query-service: query-service:
image: signoz/query-service:0.59.0 image: signoz/query-service:0.60.0
command: command:
[ [
"-config=/root/config/prometheus.yml", "-config=/root/config/prometheus.yml",
@ -186,7 +186,7 @@ services:
<<: *db-depend <<: *db-depend
frontend: frontend:
image: signoz/frontend:0.59.0 image: signoz/frontend:0.60.0
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure
@ -199,7 +199,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector: otel-collector:
image: signoz/signoz-otel-collector:0.111.9 image: signoz/signoz-otel-collector:0.111.13
command: command:
[ [
"--config=/etc/otel-collector-config.yaml", "--config=/etc/otel-collector-config.yaml",
@ -237,13 +237,15 @@ services:
- query-service - query-service
otel-collector-migrator: otel-collector-migrator:
image: signoz/signoz-schema-migrator:0.111.9 image: signoz/signoz-schema-migrator:0.111.13
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure
delay: 5s delay: 5s
command: command:
- "sync"
- "--dsn=tcp://clickhouse:9000" - "--dsn=tcp://clickhouse:9000"
- "--up="
depends_on: depends_on:
- clickhouse - clickhouse
# - clickhouse-2 # - clickhouse-2

View File

@ -69,7 +69,7 @@ services:
- --storage.path=/data - --storage.path=/data
otel-collector-migrator: otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.9} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.13}
container_name: otel-migrator container_name: otel-migrator
command: command:
- "--dsn=tcp://clickhouse:9000" - "--dsn=tcp://clickhouse:9000"
@ -84,7 +84,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
otel-collector: otel-collector:
container_name: signoz-otel-collector container_name: signoz-otel-collector
image: signoz/signoz-otel-collector:0.111.9 image: signoz/signoz-otel-collector:0.111.13
command: command:
[ [
"--config=/etc/otel-collector-config.yaml", "--config=/etc/otel-collector-config.yaml",

View File

@ -162,7 +162,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service: query-service:
image: signoz/query-service:${DOCKER_TAG:-0.59.0} image: signoz/query-service:${DOCKER_TAG:-0.60.0}
container_name: signoz-query-service container_name: signoz-query-service
command: command:
[ [
@ -201,7 +201,7 @@ services:
<<: *db-depend <<: *db-depend
frontend: frontend:
image: signoz/frontend:${DOCKER_TAG:-0.59.0} image: signoz/frontend:${DOCKER_TAG:-0.60.0}
container_name: signoz-frontend container_name: signoz-frontend
restart: on-failure restart: on-failure
depends_on: depends_on:
@ -213,7 +213,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator-sync: otel-collector-migrator-sync:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.9} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.13}
container_name: otel-migrator-sync container_name: otel-migrator-sync
command: command:
- "sync" - "sync"
@ -228,7 +228,7 @@ services:
# condition: service_healthy # condition: service_healthy
otel-collector-migrator-async: otel-collector-migrator-async:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.9} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.13}
container_name: otel-migrator-async container_name: otel-migrator-async
command: command:
- "async" - "async"
@ -245,7 +245,7 @@ services:
# condition: service_healthy # condition: service_healthy
otel-collector: otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.9} image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.13}
container_name: signoz-otel-collector container_name: signoz-otel-collector
command: command:
[ [

View File

@ -167,7 +167,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service: query-service:
image: signoz/query-service:${DOCKER_TAG:-0.59.0} image: signoz/query-service:${DOCKER_TAG:-0.60.0}
container_name: signoz-query-service container_name: signoz-query-service
command: command:
[ [
@ -208,7 +208,7 @@ services:
<<: *db-depend <<: *db-depend
frontend: frontend:
image: signoz/frontend:${DOCKER_TAG:-0.59.0} image: signoz/frontend:${DOCKER_TAG:-0.60.0}
container_name: signoz-frontend container_name: signoz-frontend
restart: on-failure restart: on-failure
depends_on: depends_on:
@ -220,7 +220,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator: otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.9} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.13}
container_name: otel-migrator container_name: otel-migrator
command: command:
- "--dsn=tcp://clickhouse:9000" - "--dsn=tcp://clickhouse:9000"
@ -234,7 +234,7 @@ services:
otel-collector: otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.9} image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.13}
container_name: signoz-otel-collector container_name: signoz-otel-collector
command: command:
[ [

View File

@ -40,6 +40,7 @@ type APIHandlerOptions struct {
// Querier Influx Interval // Querier Influx Interval
FluxInterval time.Duration FluxInterval time.Duration
UseLogsNewSchema bool UseLogsNewSchema bool
UseTraceNewSchema bool
UseLicensesV3 bool UseLicensesV3 bool
} }
@ -66,6 +67,7 @@ func NewAPIHandler(opts APIHandlerOptions) (*APIHandler, error) {
Cache: opts.Cache, Cache: opts.Cache,
FluxInterval: opts.FluxInterval, FluxInterval: opts.FluxInterval,
UseLogsNewSchema: opts.UseLogsNewSchema, UseLogsNewSchema: opts.UseLogsNewSchema,
UseTraceNewSchema: opts.UseTraceNewSchema,
UseLicensesV3: opts.UseLicensesV3, UseLicensesV3: opts.UseLicensesV3,
}) })

View File

@ -84,6 +84,13 @@ func (ah *APIHandler) listLicenses(w http.ResponseWriter, r *http.Request) {
} }
func (ah *APIHandler) applyLicense(w http.ResponseWriter, r *http.Request) { func (ah *APIHandler) applyLicense(w http.ResponseWriter, r *http.Request) {
if ah.UseLicensesV3 {
// if the licenses v3 is toggled on then do not apply license in v2 and run the validator!
// TODO: remove after migration to v3 and deprecation from zeus
zap.L().Info("early return from apply license v2 call")
render.Success(w, http.StatusOK, nil)
return
}
var l model.License var l model.License
if err := json.NewDecoder(r.Body).Decode(&l); err != nil { if err := json.NewDecoder(r.Body).Decode(&l); err != nil {

View File

@ -2,32 +2,31 @@ package api
import ( import (
"net/http" "net/http"
"go.signoz.io/signoz/ee/query-service/app/db"
"go.signoz.io/signoz/ee/query-service/model"
baseapp "go.signoz.io/signoz/pkg/query-service/app"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
"go.uber.org/zap"
) )
func (ah *APIHandler) searchTraces(w http.ResponseWriter, r *http.Request) { func (ah *APIHandler) searchTraces(w http.ResponseWriter, r *http.Request) {
if !ah.CheckFeature(basemodel.SmartTraceDetail) {
zap.L().Info("SmartTraceDetail feature is not enabled in this plan")
ah.APIHandler.SearchTraces(w, r) ah.APIHandler.SearchTraces(w, r)
return return
}
searchTracesParams, err := baseapp.ParseSearchTracesParams(r)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, "Error reading params")
return
}
result, err := ah.opts.DataConnector.SearchTraces(r.Context(), searchTracesParams, db.SmartTraceAlgorithm) // This is commented since this will be taken care by new trace API
if ah.HandleError(w, err, http.StatusBadRequest) {
return
}
ah.WriteJSON(w, r, result) // if !ah.CheckFeature(basemodel.SmartTraceDetail) {
// zap.L().Info("SmartTraceDetail feature is not enabled in this plan")
// ah.APIHandler.SearchTraces(w, r)
// return
// }
// searchTracesParams, err := baseapp.ParseSearchTracesParams(r)
// if err != nil {
// RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, "Error reading params")
// return
// }
// result, err := ah.opts.DataConnector.SearchTraces(r.Context(), searchTracesParams, db.SmartTraceAlgorithm)
// if ah.HandleError(w, err, http.StatusBadRequest) {
// return
// }
// ah.WriteJSON(w, r, result)
} }

View File

@ -26,8 +26,9 @@ func NewDataConnector(
dialTimeout time.Duration, dialTimeout time.Duration,
cluster string, cluster string,
useLogsNewSchema bool, useLogsNewSchema bool,
useTraceNewSchema bool,
) *ClickhouseReader { ) *ClickhouseReader {
ch := basechr.NewReader(localDB, promConfigPath, lm, maxIdleConns, maxOpenConns, dialTimeout, cluster, useLogsNewSchema) ch := basechr.NewReader(localDB, promConfigPath, lm, maxIdleConns, maxOpenConns, dialTimeout, cluster, useLogsNewSchema, useTraceNewSchema)
return &ClickhouseReader{ return &ClickhouseReader{
conn: ch.GetConn(), conn: ch.GetConn(),
appdb: localDB, appdb: localDB,

View File

@ -77,6 +77,7 @@ type ServerOptions struct {
Cluster string Cluster string
GatewayUrl string GatewayUrl string
UseLogsNewSchema bool UseLogsNewSchema bool
UseTraceNewSchema bool
UseLicensesV3 bool UseLicensesV3 bool
} }
@ -156,6 +157,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
serverOptions.DialTimeout, serverOptions.DialTimeout,
serverOptions.Cluster, serverOptions.Cluster,
serverOptions.UseLogsNewSchema, serverOptions.UseLogsNewSchema,
serverOptions.UseTraceNewSchema,
) )
go qb.Start(readerReady) go qb.Start(readerReady)
reader = qb reader = qb
@ -189,6 +191,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
serverOptions.DisableRules, serverOptions.DisableRules,
lm, lm,
serverOptions.UseLogsNewSchema, serverOptions.UseLogsNewSchema,
serverOptions.UseTraceNewSchema,
) )
if err != nil { if err != nil {
@ -270,6 +273,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
FluxInterval: fluxInterval, FluxInterval: fluxInterval,
Gateway: gatewayProxy, Gateway: gatewayProxy,
UseLogsNewSchema: serverOptions.UseLogsNewSchema, UseLogsNewSchema: serverOptions.UseLogsNewSchema,
UseTraceNewSchema: serverOptions.UseTraceNewSchema,
UseLicensesV3: serverOptions.UseLicensesV3, UseLicensesV3: serverOptions.UseLicensesV3,
} }
@ -313,10 +317,10 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server,
r := baseapp.NewRouter() r := baseapp.NewRouter()
r.Use(baseapp.LogCommentEnricher)
r.Use(setTimeoutMiddleware) r.Use(setTimeoutMiddleware)
r.Use(s.analyticsMiddleware) r.Use(s.analyticsMiddleware)
r.Use(loggingMiddlewarePrivate) r.Use(loggingMiddlewarePrivate)
r.Use(baseapp.LogCommentEnricher)
apiHandler.RegisterPrivateRoutes(r) apiHandler.RegisterPrivateRoutes(r)
@ -356,10 +360,10 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
} }
am := baseapp.NewAuthMiddleware(getUserFromRequest) am := baseapp.NewAuthMiddleware(getUserFromRequest)
r.Use(baseapp.LogCommentEnricher)
r.Use(setTimeoutMiddleware) r.Use(setTimeoutMiddleware)
r.Use(s.analyticsMiddleware) r.Use(s.analyticsMiddleware)
r.Use(loggingMiddleware) r.Use(loggingMiddleware)
r.Use(baseapp.LogCommentEnricher)
apiHandler.RegisterRoutes(r, am) apiHandler.RegisterRoutes(r, am)
apiHandler.RegisterLogsRoutes(r, am) apiHandler.RegisterLogsRoutes(r, am)
@ -737,7 +741,8 @@ func makeRulesManager(
cache cache.Cache, cache cache.Cache,
disableRules bool, disableRules bool,
fm baseint.FeatureLookup, fm baseint.FeatureLookup,
useLogsNewSchema bool) (*baserules.Manager, error) { useLogsNewSchema bool,
useTraceNewSchema bool) (*baserules.Manager, error) {
// create engine // create engine
pqle, err := pqle.FromConfigPath(promConfigPath) pqle, err := pqle.FromConfigPath(promConfigPath)
@ -767,8 +772,9 @@ func makeRulesManager(
EvalDelay: baseconst.GetEvalDelay(), EvalDelay: baseconst.GetEvalDelay(),
PrepareTaskFunc: rules.PrepareTaskFunc, PrepareTaskFunc: rules.PrepareTaskFunc,
PrepareTestRuleFunc: rules.TestNotification,
UseLogsNewSchema: useLogsNewSchema, UseLogsNewSchema: useLogsNewSchema,
UseTraceNewSchema: useTraceNewSchema,
PrepareTestRuleFunc: rules.TestNotification,
} }
// create Manager // create Manager

View File

@ -19,12 +19,14 @@ import (
// Repo is license repo. stores license keys in a secured DB // Repo is license repo. stores license keys in a secured DB
type Repo struct { type Repo struct {
db *sqlx.DB db *sqlx.DB
useLicensesV3 bool
} }
// NewLicenseRepo initiates a new license repo // NewLicenseRepo initiates a new license repo
func NewLicenseRepo(db *sqlx.DB) Repo { func NewLicenseRepo(db *sqlx.DB, useLicensesV3 bool) Repo {
return Repo{ return Repo{
db: db, db: db,
useLicensesV3: useLicensesV3,
} }
} }
@ -78,9 +80,7 @@ func (r *Repo) GetLicensesV3(ctx context.Context) ([]*model.LicenseV3, error) {
return licenseV3Data, nil return licenseV3Data, nil
} }
// GetActiveLicense fetches the latest active license from DB. func (r *Repo) GetActiveLicenseV2(ctx context.Context) (*model.License, *basemodel.ApiError) {
// 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) {
var err error var err error
licenses := []model.License{} licenses := []model.License{}
@ -109,6 +109,31 @@ func (r *Repo) GetActiveLicense(ctx context.Context) (*model.License, *basemodel
return active, nil 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) {
if r.useLicensesV3 {
zap.L().Info("Using licenses v3 for GetActiveLicense")
activeLicenseV3, err := r.GetActiveLicenseV3(ctx)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("failed to get active licenses from db: %v", err))
}
if activeLicenseV3 == nil {
return nil, nil
}
activeLicenseV2 := model.ConvertLicenseV3ToLicenseV2(activeLicenseV3)
return activeLicenseV2, nil
}
active, err := r.GetActiveLicenseV2(ctx)
if err != nil {
return nil, err
}
return active, nil
}
func (r *Repo) GetActiveLicenseV3(ctx context.Context) (*model.LicenseV3, error) { func (r *Repo) GetActiveLicenseV3(ctx context.Context) (*model.LicenseV3, error) {
var err error var err error
licenses := []model.LicenseDB{} licenses := []model.LicenseDB{}

View File

@ -56,7 +56,7 @@ func StartManager(dbType string, db *sqlx.DB, useLicensesV3 bool, features ...ba
return LM, nil return LM, nil
} }
repo := NewLicenseRepo(db) repo := NewLicenseRepo(db, useLicensesV3)
err := repo.InitDB(dbType) err := repo.InitDB(dbType)
if err != nil { if err != nil {
@ -69,7 +69,7 @@ func StartManager(dbType string, db *sqlx.DB, useLicensesV3 bool, features ...ba
if useLicensesV3 { if useLicensesV3 {
// get active license from the db // get active license from the db
active, err := m.repo.GetActiveLicense(context.Background()) active, err := m.repo.GetActiveLicenseV2(context.Background())
if err != nil { if err != nil {
return m, err return m, err
} }
@ -88,6 +88,7 @@ func StartManager(dbType string, db *sqlx.DB, useLicensesV3 bool, features ...ba
if apiError != nil && apiError.Typ != model.ErrorConflict { if apiError != nil && apiError.Typ != model.ErrorConflict {
return m, apiError return m, apiError
} }
zap.L().Info("Successfully inserted license from v2 to v3 table")
} }
} }
@ -266,6 +267,7 @@ func (lm *Manager) GetLicensesV3(ctx context.Context) (response []*model.License
// Validator validates license after an epoch of time // Validator validates license after an epoch of time
func (lm *Manager) Validator(ctx context.Context) { func (lm *Manager) Validator(ctx context.Context) {
zap.L().Info("Validator started!")
defer close(lm.terminated) defer close(lm.terminated)
tick := time.NewTicker(validationFrequency) tick := time.NewTicker(validationFrequency)
defer tick.Stop() defer tick.Stop()
@ -290,6 +292,7 @@ func (lm *Manager) Validator(ctx context.Context) {
// Validator validates license after an epoch of time // Validator validates license after an epoch of time
func (lm *Manager) ValidatorV3(ctx context.Context) { func (lm *Manager) ValidatorV3(ctx context.Context) {
zap.L().Info("ValidatorV3 started!")
defer close(lm.terminated) defer close(lm.terminated)
tick := time.NewTicker(validationFrequency) tick := time.NewTicker(validationFrequency)
defer tick.Stop() defer tick.Stop()
@ -379,7 +382,6 @@ func (lm *Manager) Validate(ctx context.Context) (reterr error) {
return nil return nil
} }
// todo[vikrantgupta25]: check the comparison here between old and new license!
func (lm *Manager) RefreshLicense(ctx context.Context) *model.ApiError { func (lm *Manager) RefreshLicense(ctx context.Context) *model.ApiError {
license, apiError := validate.ValidateLicenseV3(lm.activeLicenseV3.Key) license, apiError := validate.ValidateLicenseV3(lm.activeLicenseV3.Key)

View File

@ -94,6 +94,7 @@ func main() {
var cluster string var cluster string
var useLogsNewSchema bool var useLogsNewSchema bool
var useTraceNewSchema bool
var useLicensesV3 bool var useLicensesV3 bool
var cacheConfigPath, fluxInterval string var cacheConfigPath, fluxInterval string
var enableQueryServiceLogOTLPExport bool var enableQueryServiceLogOTLPExport bool
@ -105,6 +106,7 @@ func main() {
var gatewayUrl string var gatewayUrl string
flag.BoolVar(&useLogsNewSchema, "use-logs-new-schema", false, "use logs_v2 schema for logs") flag.BoolVar(&useLogsNewSchema, "use-logs-new-schema", false, "use logs_v2 schema for logs")
flag.BoolVar(&useTraceNewSchema, "use-trace-new-schema", false, "use new schema for traces")
flag.BoolVar(&useLicensesV3, "use-licenses-v3", false, "use licenses_v3 schema for licenses") flag.BoolVar(&useLicensesV3, "use-licenses-v3", false, "use licenses_v3 schema for licenses")
flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)") flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)")
flag.StringVar(&skipTopLvlOpsPath, "skip-top-level-ops", "", "(config file to skip top level operations)") flag.StringVar(&skipTopLvlOpsPath, "skip-top-level-ops", "", "(config file to skip top level operations)")
@ -145,6 +147,7 @@ func main() {
Cluster: cluster, Cluster: cluster,
GatewayUrl: gatewayUrl, GatewayUrl: gatewayUrl,
UseLogsNewSchema: useLogsNewSchema, UseLogsNewSchema: useLogsNewSchema,
UseTraceNewSchema: useTraceNewSchema,
UseLicensesV3: useLicensesV3, UseLicensesV3: useLicensesV3,
} }

View File

@ -247,3 +247,24 @@ func NewLicenseV3WithIDAndKey(id string, key string, data map[string]interface{}
licenseDataWithIdAndKey["key"] = key licenseDataWithIdAndKey["key"] = key
return NewLicenseV3(licenseDataWithIdAndKey) return NewLicenseV3(licenseDataWithIdAndKey)
} }
func ConvertLicenseV3ToLicenseV2(l *LicenseV3) *License {
planKeyFromPlanName, ok := MapOldPlanKeyToNewPlanName[l.PlanName]
if !ok {
planKeyFromPlanName = Basic
}
return &License{
Key: l.Key,
ActivationId: "",
PlanDetails: "",
FeatureSet: l.Features,
ValidationMessage: "",
IsCurrent: l.IsCurrent,
LicensePlan: LicensePlan{
PlanKey: planKeyFromPlanName,
ValidFrom: l.ValidFrom,
ValidUntil: l.ValidUntil,
Status: l.Status},
}
}

View File

@ -26,6 +26,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
opts.FF, opts.FF,
opts.Reader, opts.Reader,
opts.UseLogsNewSchema, opts.UseLogsNewSchema,
opts.UseTraceNewSchema,
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay), baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
) )
@ -122,6 +123,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
opts.FF, opts.FF,
opts.Reader, opts.Reader,
opts.UseLogsNewSchema, opts.UseLogsNewSchema,
opts.UseTraceNewSchema,
baserules.WithSendAlways(), baserules.WithSendAlways(),
baserules.WithSendUnmatched(), baserules.WithSendUnmatched(),
) )

View File

@ -128,7 +128,7 @@
"uuid": "^8.3.2", "uuid": "^8.3.2",
"web-vitals": "^0.2.4", "web-vitals": "^0.2.4",
"webpack": "5.94.0", "webpack": "5.94.0",
"webpack-dev-server": "^4.15.1", "webpack-dev-server": "^4.15.2",
"webpack-retry-chunk-load-plugin": "3.1.1", "webpack-retry-chunk-load-plugin": "3.1.1",
"xstate": "^4.31.0" "xstate": "^4.31.0"
}, },
@ -241,6 +241,7 @@
"semver": "7.5.4", "semver": "7.5.4",
"xml2js": "0.5.0", "xml2js": "0.5.0",
"phin": "^3.7.1", "phin": "^3.7.1",
"body-parser": "1.20.3" "body-parser": "1.20.3",
"http-proxy-middleware": "3.0.3"
} }
} }

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none"><path fill="#A65F3E" d="M8.04 10.331a.41.41 0 0 1-.414-.414.4.4 0 0 1 .121-.292l8.071-8.071a.414.414 0 1 1 .585.585l-8.07 8.071a.4.4 0 0 1-.293.121"/><path fill="#A65F3E" d="M16.11 1.5c.09 0 .178.034.245.101a.35.35 0 0 1 0 .492l-8.07 8.07a.346.346 0 0 1-.49 0 .35.35 0 0 1 0-.49l8.07-8.072a.35.35 0 0 1 .245-.101m0-.133a.48.48 0 0 0-.338.14L7.7 9.578a.47.47 0 0 0-.14.34.475.475 0 0 0 .478.478c.13 0 .25-.05.34-.14l8.07-8.071a.48.48 0 0 0-.339-.818"/><path fill="#FFE082" d="m1.701 12.438 3.89 3.889c.873-.963 1.62-2.057 2.023-3.313.03-.091.034-.24.128-.359.451-.566 1.865-2.008.706-3.167-1.106-1.106-2.438.227-2.994.686-.17.14-.384.228-.606.276-1.493.326-3.034 1.869-3.147 1.988"/><path fill="#FFE082" d="M8.385 8.577a.62.62 0 0 1 .393-.085c.098.018.237.135.38.28.144.143.28.304.32.408s-.005.242-.005.242c-.116.23-.383.69-.6.624-.24-.074-.482-.305-.66-.479a1.5 1.5 0 0 1-.276-.328c-.096-.177.008-.324.129-.447.086-.082.232-.17.319-.215"/><path fill="#F9C248" d="M8.327 8.975c.116.11.21.243.339.338.252.185.455.097.62-.052.049-.044.122-.1.17-.055a.1.1 0 0 1 .025.051.45.45 0 0 1-.045.273 1.3 1.3 0 0 1-.433.529c-.032.022-.07.044-.11.032a.12.12 0 0 1-.056-.045c-.207-.244-.37-.533-.626-.724-.103-.076-.364-.132-.298-.303.1-.262.317-.137.414-.044"/><path fill="#F9C248" d="M7.614 13.014c.028-.091.033-.24.127-.359.515-.645 1.223-1.38 1.145-2.275-.01-.123-.169-.75-.342-.514-.04.052-.024.315-.03.379-.1 1.172-1.02 1.821-1.19 2.024s-.164.393-.31.695a5 5 0 0 1-.61.947c-.379.47-.825.88-1.286 1.27a.8.8 0 0 0-.203.217c-.131.241.153.406.305.558l.369.368c.873-.961 1.62-2.055 2.025-3.31"/><path fill="#E2A610" d="M5.537 15.809c-.1-.157-.242-.3-.317-.458a.24.24 0 0 1-.03-.123c.01-.08.13-.15.187-.198q.129-.108.254-.22c.162-.149.314-.314.419-.509.017-.031.032-.07.016-.102-.035-.065-.238.152-.275.186-.105.092-.208.187-.318.272-.146.113-.422.304-.618.213-.1-.046-.19-.169-.263-.249-.084-.094-.164-.191-.252-.283a17 17 0 0 0-.592-.582c-.05-.046-.06-.066-.003-.122a10 10 0 0 0 .546-.58c.022-.025.044-.067.017-.09-.018-.015-.048-.004-.07.007-.26.138-.467.354-.692.544-.055.046-.214-.13-.249-.158-.092-.073-.154-.102-.046-.21.484-.49.972-.946 1.554-1.323.107-.07.22-.14.28-.253-.01-.03-.054-.026-.085-.015-.807.29-1.89 1.291-1.983 1.38-.162.158-.454-.206-.885-.481-.147-.094 0-.235.038-.279.26-.307.603-.642.603-.642-.127.013-.956.76-1.054.873-.084.097-.17.184-.175.318a.52.52 0 0 0 .107.325c.77 1.05 2.586 2.794 3.23 3.253.384.274.502.224.659.068a.35.35 0 0 0 .105-.3.65.65 0 0 0-.108-.263"/><path fill="#A65F3E" d="M8.835 10.176s-.438-.825-1.017-1.074c0 0-.02-.054.02-.1.052-.057.12-.07.157-.046.427.265.812.619 1.007 1.102.039.093-.131.23-.167.118"/><path fill="#F44336" d="M7.64 12.88c-.528-.818-1.63-1.937-2.46-2.524-.066-.046.204-.204.272-.156a9.7 9.7 0 0 1 2.31 2.398c.045.067-.091.33-.123.282M8.193 12.078c-.506-.71-1.521-1.738-2.238-2.312-.062-.05.182-.22.232-.181.755.602 1.668 1.499 2.18 2.263.037.057-.138.282-.174.23"/></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="16" fill="none"><path fill="#616161" fill-rule="evenodd" d="M8.096 2.885H4.372V2.51h3.724zM8.096 4.79H4.372v-.375h3.724z" clip-rule="evenodd"/><path fill="#9E9E9E" d="M7.098 15.539H5.662V.936s.134-.311.719-.311.719.311.719.311v14.603z"/><path fill="#757575" d="M6.73.671V12.47H5.662v1.074c.181.001.345.023.493.055.336.074.576.37.576.714v1.227H7.1V.936c-.002 0-.08-.179-.37-.265"/><path fill="#2196F3" d="M10.58.54a3.03 3.03 0 0 0-3.028 3.038 3.02 3.02 0 0 0 3.027 3.028 3.025 3.025 0 0 0 3.028-3.028A3.035 3.035 0 0 0 10.579.54"/><path fill="#fff" d="M11.902 1.671c-.19-.048-.569-.098-1.321-.098-.753 0-1.132.05-1.322.098-.112.029-.488.185-.488.606v2.598c0 .142.115.258.258.258h.095v.288c0 .084.068.151.152.151h.306a.15.15 0 0 0 .151-.151v-.288h1.693v.288c0 .084.067.151.15.151h.307a.15.15 0 0 0 .151-.151v-.288h.098a.26.26 0 0 0 .259-.258V2.277c0-.404-.377-.579-.49-.606m-2.139.206c0-.064.051-.115.115-.115h1.403c.063 0 .115.051.115.115v.204a.115.115 0 0 1-.115.115H9.878a.115.115 0 0 1-.115-.115zm.024 2.736a.08.08 0 0 1-.078.078h-.308a.264.264 0 0 1-.264-.264v-.139c0-.042.035-.077.077-.077h.31c.144 0 .263.117.263.264zm2.235-.186a.264.264 0 0 1-.264.264h-.309a.08.08 0 0 1-.077-.078v-.138c0-.145.117-.264.264-.264h.308c.043 0 .078.035.078.077zm.07-1.129c0 .168-.363.46-1.513.46s-1.512-.27-1.512-.46v-.767c0-.05.05-.175.175-.175h2.695c.125 0 .155.126.155.175z"/><path fill="#F5F5F5" d="M8.61 12.867H4.15a.285.285 0 0 1-.285-.285v-5.15c0-.158.127-.285.285-.285h4.457c.158 0 .285.127.285.285v5.15a.285.285 0 0 1-.284.285"/><path fill="#82AEC0" d="M8.128 12.015H4.632l-.01-4.07H8.12z" opacity=".8"/><path fill="#F5F5F5" fill-rule="evenodd" d="M6.246 12.07V7.945h.25v4.123z" clip-rule="evenodd"/><path fill="#616161" d="M6.246 7.946H4.622v.34h1.624z"/><path fill="#F5F5F5" fill-rule="evenodd" d="M8.142 11.307H4.618v-.125h3.524zM8.142 10.482H4.618v-.125h3.524zM8.12 9.657H4.621v-.125H8.12zM8.12 8.833H4.617v-.125H8.12z" clip-rule="evenodd"/><path fill="#616161" d="M8.118 9.426H6.495v.34h1.623zM6.253 10.25H4.635v.34h1.618z"/><path fill="#9E9E9E" fill-rule="evenodd" d="M4.15 7.334a.097.097 0 0 0-.097.098v5.15c0 .054.044.097.098.097h4.458a.097.097 0 0 0 .097-.097v-5.15a.097.097 0 0 0-.098-.098zm-.472.098c0-.261.212-.473.473-.473h4.457c.261 0 .473.212.473.473v5.15c0 .26-.211.472-.472.472H4.151a.47.47 0 0 1-.473-.472z" clip-rule="evenodd"/><path fill="#757575" d="M4.17 12.682c-.194.017-.194-.11-.194-.145V7.493c0-.141.115-.256.256-.256H8.56c.118 0 .172.071.148.216 0 0-.015-.092-.128-.092H4.233a.133.133 0 0 0-.132.132v5.045c0 .117.069.144.069.144"/><path fill="#FFCA28" d="M4.9 1.775H.642v3.7h4.26z"/><path fill="#9E9E9E" d="M4.663 1.775c.132 0 .238.106.238.237v3.225a.237.237 0 0 1-.238.237H.88a.237.237 0 0 1-.238-.237V2.012c0-.131.107-.237.238-.237zm0-.25H.88a.49.49 0 0 0-.488.487v3.225c0 .269.22.487.488.487h3.783a.49.49 0 0 0 .488-.487V2.012a.487.487 0 0 0-.488-.487"/><path fill="#FFFDE7" fill-rule="evenodd" d="M4.902 3.11H.642v-.25h4.26zM4.902 4.388H.642v-.25h4.26z" clip-rule="evenodd"/><path fill="#757575" d="M1.975 2.186H.904v.282h1.07zM1.711 4.777H.904v.283h.807zM4.552 4.777h-.807v.283h.807zM4.552 2.186h-.807v.282h.807zM3.795 3.482H3.33v.282h.465zM4.552 3.482h-.465v.282h.465zM2.388 3.482H.904v.282h1.484z"/></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,7 @@
{
"containers_visualization_message": "The ability to visualise containers is in active development and should be available to you soon.",
"processes_visualization_message": "The ability to visualise processes is in active development and should be available to you soon.",
"working_message": "We're working to extend infrastructure monitoring to take care of a bunch of different cases. Thank you for your patience.",
"waitlist_message": "Join the waitlist for early access or contact support.",
"contact_support": "Contact Support"
}

View File

@ -40,5 +40,6 @@
"SUPPORT": "SigNoz | Support", "SUPPORT": "SigNoz | Support",
"DEFAULT": "Open source Observability Platform | SigNoz", "DEFAULT": "Open source Observability Platform | SigNoz",
"ALERT_HISTORY": "SigNoz | Alert Rule History", "ALERT_HISTORY": "SigNoz | Alert Rule History",
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview" "ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring"
} }

View File

@ -7,5 +7,5 @@
"save": "Save", "save": "Save",
"edit": "Edit", "edit": "Edit",
"logged_in": "Logged In", "logged_in": "Logged In",
"pending_data_placeholder": "Just a bit of patience, just a little bits enough ⎯ were getting your {{dataSource}}!" "pending_data_placeholder": "Retrieving your {{dataSource}}!"
} }

View File

@ -0,0 +1,7 @@
{
"containers_visualization_message": "The ability to visualise containers is in active development and should be available to you soon.",
"processes_visualization_message": "The ability to visualise processes is in active development and should be available to you soon.",
"working_message": "We're working to extend infrastructure monitoring to take care of a bunch of different cases. Thank you for your patience.",
"waitlist_message": "Join the waitlist for early access or contact support.",
"contact_support": "Contact Support"
}

View File

@ -53,5 +53,6 @@
"INTEGRATIONS": "SigNoz | Integrations", "INTEGRATIONS": "SigNoz | Integrations",
"ALERT_HISTORY": "SigNoz | Alert Rule History", "ALERT_HISTORY": "SigNoz | Alert Rule History",
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview", "ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
"MESSAGING_QUEUES": "SigNoz | Messaging Queues" "MESSAGING_QUEUES": "SigNoz | Messaging Queues",
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring"
} }

View File

@ -128,6 +128,18 @@ function App(): JSX.Element {
setRoutes(newRoutes); setRoutes(newRoutes);
} }
const isInfraMonitoringEnabled =
allFlags.find((flag) => flag.name === FeatureKeys.HOSTS_INFRA_MONITORING)
?.active || false;
if (!isInfraMonitoringEnabled) {
const newRoutes = routes.filter(
(route) => route?.path !== ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
);
setRoutes(newRoutes);
}
}); });
const isOnBasicPlan = const isOnBasicPlan =

View File

@ -228,3 +228,10 @@ export const MQDetailPage = Loadable(
/* webpackChunkName: "MQDetailPage" */ 'pages/MessagingQueues/MQDetailPage' /* webpackChunkName: "MQDetailPage" */ 'pages/MessagingQueues/MQDetailPage'
), ),
); );
export const InfrastructureMonitoring = Loadable(
() =>
import(
/* webpackChunkName: "InfrastructureMonitoring" */ 'pages/InfrastructureMonitoring'
),
);

View File

@ -15,6 +15,7 @@ import {
EditAlertChannelsAlerts, EditAlertChannelsAlerts,
EditRulesPage, EditRulesPage,
ErrorDetails, ErrorDetails,
InfrastructureMonitoring,
IngestionSettings, IngestionSettings,
InstalledIntegrations, InstalledIntegrations,
LicensePage, LicensePage,
@ -391,6 +392,13 @@ const routes: AppRoutes[] = [
key: 'MESSAGING_QUEUES_DETAIL', key: 'MESSAGING_QUEUES_DETAIL',
isPrivate: true, isPrivate: true,
}, },
{
path: ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
exact: true,
component: InfrastructureMonitoring,
key: 'INFRASTRUCTURE_MONITORING_HOSTS',
isPrivate: true,
},
]; ];
export const SUPPORT_ROUTE: AppRoutes = { export const SUPPORT_ROUTE: AppRoutes = {

View File

@ -0,0 +1,37 @@
import { ApiBaseInstance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError, AxiosResponse } from 'axios';
import { baseAutoCompleteIdKeysOrder } from 'constants/queryBuilder';
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
BaseAutocompleteData,
IQueryAutocompleteResponse,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
export const getHostAttributeKeys = async (
searchText = '',
): Promise<SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse> => {
try {
const response: AxiosResponse<{
data: IQueryAutocompleteResponse;
}> = await ApiBaseInstance.get(
`/hosts/attribute_keys?dataSource=metrics&searchText=${searchText}`,
);
const payload: BaseAutocompleteData[] =
response.data.data.attributeKeys?.map(({ id: _, ...item }) => ({
...item,
id: createIdFromObjectFields(item, baseAutoCompleteIdKeysOrder),
})) || [];
return {
statusCode: 200,
error: null,
message: response.statusText,
payload: { attributeKeys: payload },
};
} catch (e) {
return ErrorResponseHandler(e as AxiosError);
}
};

View File

@ -0,0 +1,75 @@
import { ApiBaseInstance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
export interface HostListPayload {
filters: TagFilter;
groupBy: BaseAutocompleteData[];
offset?: number;
limit?: number;
orderBy?: {
columnName: string;
order: 'asc' | 'desc';
};
}
export interface TimeSeriesValue {
timestamp: number;
value: string;
}
export interface TimeSeries {
labels: Record<string, string>;
labelsArray: Array<Record<string, string>>;
values: TimeSeriesValue[];
}
export interface HostData {
hostName: string;
active: boolean;
os: string;
cpu: number;
cpuTimeSeries: TimeSeries;
memory: number;
memoryTimeSeries: TimeSeries;
wait: number;
waitTimeSeries: TimeSeries;
load15: number;
load15TimeSeries: TimeSeries;
}
export interface HostListResponse {
status: string;
data: {
type: string;
records: HostData[];
groups: null;
total: number;
};
}
export const getHostLists = async (
props: HostListPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<HostListResponse> | ErrorResponse> => {
try {
const response = await ApiBaseInstance.post('/hosts/list', props, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
params: props,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@ -0,0 +1,38 @@
import { ApiBaseInstance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import createQueryParams from 'lib/createQueryParams';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
IAttributeValuesResponse,
IGetAttributeValuesPayload,
} from 'types/api/queryBuilder/getAttributesValues';
export const getInfraAttributesValues = async ({
dataSource,
attributeKey,
filterAttributeKeyDataType,
tagType,
searchText,
}: IGetAttributeValuesPayload): Promise<
SuccessResponse<IAttributeValuesResponse> | ErrorResponse
> => {
try {
const response = await ApiBaseInstance.get(
`/hosts/attribute_values?${createQueryParams({
dataSource,
attributeKey,
searchText,
})}&filterAttributeKeyDataType=${filterAttributeKeyDataType}&tagType=${tagType}`,
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@ -5,7 +5,6 @@ import { baseAutoCompleteIdKeysOrder } from 'constants/queryBuilder';
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields'; import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
import createQueryParams from 'lib/createQueryParams'; import createQueryParams from 'lib/createQueryParams';
import { ErrorResponse, SuccessResponse } from 'types/api'; import { ErrorResponse, SuccessResponse } from 'types/api';
// ** Types
import { IGetAttributeKeysPayload } from 'types/api/queryBuilder/getAttributeKeys'; import { IGetAttributeKeysPayload } from 'types/api/queryBuilder/getAttributeKeys';
import { import {
BaseAutocompleteData, BaseAutocompleteData,

View File

@ -120,7 +120,6 @@ function CustomTimePicker({
useEffect(() => { useEffect(() => {
const value = getSelectedTimeRangeLabel(selectedTime, selectedValue); const value = getSelectedTimeRangeLabel(selectedTime, selectedValue);
setSelectedTimePlaceholderValue(value); setSelectedTimePlaceholderValue(value);
}, [selectedTime, selectedValue]); }, [selectedTime, selectedValue]);

View File

@ -0,0 +1,48 @@
.host-containers {
max-width: 600px;
margin: 150px auto;
padding: 0 16px;
.infra-container-card {
display: flex;
flex-direction: column;
justify-content: center;
}
.infra-container-card-text {
font-size: var(--font-size-sm);
color: var(--text-vanilla-400);
line-height: 20px;
letter-spacing: -0.07px;
width: 400px;
font-family: 'Inter';
margin-top: 12px;
}
.infra-container-working-msg {
display: flex;
width: 400px;
padding: 12px;
align-items: flex-start;
gap: 12px;
border-radius: 4px;
background: rgba(171, 189, 255, 0.04);
.ant-space {
align-items: flex-start;
}
}
.infra-container-contact-support-btn {
display: flex;
align-items: center;
justify-content: center;
margin: auto;
}
}
.lightMode {
.infra-container-card-text {
color: var(--text-ink-200);
}
}

View File

@ -0,0 +1,36 @@
import './Containers.styles.scss';
import { Space, Typography } from 'antd';
import { useTranslation } from 'react-i18next';
const { Text } = Typography;
function Containers(): JSX.Element {
const { t } = useTranslation(['infraMonitoring']);
return (
<Space direction="vertical" className="host-containers" size={24}>
<div className="infra-container-card">
<img
src="/Icons/infraContainers.svg"
alt="infra-container"
width={32}
height={32}
/>
<Text className="infra-container-card-text">
{t('containers_visualization_message')}
</Text>
</div>
<div className="infra-container-working-msg">
<Space>
<img src="/Icons/broom.svg" alt="broom" width={16} height={16} />
<Text className="infra-container-card-text">{t('working_message')}</Text>
</Space>
</div>
</Space>
);
}
export default Containers;

View File

@ -0,0 +1,7 @@
import { HostData } from 'api/infraMonitoring/getHostLists';
export type HostDetailProps = {
host: HostData | null;
isModalTimeSelection: boolean;
onClose: () => void;
};

View File

@ -0,0 +1,193 @@
.host-metric-traces {
margin-top: 1rem;
.host-metric-traces-header {
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
gap: 8px;
padding: 12px;
border-radius: 3px;
border: 1px solid var(--bg-slate-500);
.filter-section {
flex: 1;
.ant-select-selector {
border-radius: 2px;
border: 1px solid var(--bg-slate-400) !important;
background-color: var(--bg-ink-300) !important;
input {
font-size: 12px;
}
.ant-tag .ant-typography {
font-size: 12px;
}
}
}
}
.host-metric-traces-table {
.ant-table-content {
overflow: hidden !important;
}
.ant-table {
border-radius: 3px;
border: 1px solid var(--bg-slate-500);
.ant-table-thead > tr > th {
padding: 12px;
font-weight: 500;
font-size: 12px;
line-height: 18px;
background: rgba(171, 189, 255, 0.01);
border-bottom: none;
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 600;
line-height: 18px; /* 163.636% */
letter-spacing: 0.44px;
text-transform: uppercase;
&::before {
background-color: transparent;
}
}
.ant-table-thead > tr > th:has(.hostname-column-header) {
background: var(--bg-ink-400);
}
.ant-table-cell {
padding: 12px;
font-size: 13px;
line-height: 20px;
color: var(--bg-vanilla-100);
background: rgba(171, 189, 255, 0.01);
}
.ant-table-cell:has(.hostname-column-value) {
background: var(--bg-ink-400);
}
.hostname-column-value {
color: var(--bg-vanilla-100);
font-family: 'Geist Mono';
font-style: normal;
font-weight: 600;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.status-cell {
.active-tag {
color: var(--bg-forest-500);
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
}
.progress-container {
.ant-progress-bg {
height: 8px !important;
border-radius: 4px;
}
}
.ant-table-tbody > tr:hover > td {
background: rgba(255, 255, 255, 0.04);
}
.ant-table-cell:first-child {
text-align: justify;
}
.ant-table-cell:nth-child(2) {
padding-left: 16px;
padding-right: 16px;
}
.ant-table-cell:nth-child(n + 3) {
padding-right: 24px;
}
.column-header-right {
text-align: right;
}
.ant-table-tbody > tr > td {
border-bottom: none;
}
.ant-table-thead
> tr
> th:not(:last-child):not(.ant-table-selection-column):not(.ant-table-row-expand-icon-cell):not([colspan])::before {
background-color: transparent;
}
.ant-empty-normal {
visibility: hidden;
}
}
.ant-table-container::after {
content: none;
}
}
}
.lightMode {
.host-metric-traces-header {
.filter-section {
border-top: 1px solid var(--bg-vanilla-300);
border-bottom: 1px solid var(--bg-vanilla-300);
.ant-select-selector {
border-color: var(--bg-vanilla-300) !important;
background-color: var(--bg-vanilla-100) !important;
color: var(--bg-ink-200);
}
}
}
.host-metric-traces-table {
.ant-table {
border-radius: 3px;
border: 1px solid var(--bg-vanilla-300);
.ant-table-thead > tr > th {
background: var(--bg-vanilla-100);
color: var(--text-ink-300);
}
.ant-table-thead > tr > th:has(.hostname-column-header) {
background: var(--bg-vanilla-100);
}
.ant-table-cell {
background: var(--bg-vanilla-100);
color: var(--bg-ink-500);
}
.ant-table-cell:has(.hostname-column-value) {
background: var(--bg-vanilla-100);
}
.hostname-column-value {
color: var(--bg-ink-300);
}
.ant-table-tbody > tr:hover > td {
background: rgba(0, 0, 0, 0.04);
}
}
}
}

View File

@ -0,0 +1,195 @@
import './HostMetricTraces.styles.scss';
import { ResizeTable } from 'components/ResizeTable';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { QueryParams } from 'constants/query';
import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch';
import NoLogs from 'container/NoLogs/NoLogs';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import { ErrorText } from 'container/TimeSeriesView/styles';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
import TraceExplorerControls from 'container/TracesExplorer/Controls';
import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs';
import { TracesLoading } from 'container/TracesExplorer/TraceLoading/TraceLoading';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { Pagination } from 'hooks/queryPagination';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { getHostTracesQueryPayload, selectedColumns } from './constants';
import { getListColumns } from './utils';
interface Props {
timeRange: {
startTime: number;
endTime: number;
};
isModalTimeSelection: boolean;
handleTimeChange: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
handleChangeTracesFilters: (value: IBuilderQuery['filters']) => void;
tracesFilters: IBuilderQuery['filters'];
selectedInterval: Time;
}
function HostMetricTraces({
timeRange,
isModalTimeSelection,
handleTimeChange,
handleChangeTracesFilters,
tracesFilters,
selectedInterval,
}: Props): JSX.Element {
const [traces, setTraces] = useState<any[]>([]);
const [offset] = useState<number>(0);
const { currentQuery } = useQueryBuilder();
const updatedCurrentQuery = useMemo(
() => ({
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
{
...currentQuery.builder.queryData[0],
dataSource: DataSource.TRACES,
aggregateOperator: 'noop',
aggregateAttribute: {
...currentQuery.builder.queryData[0].aggregateAttribute,
},
},
],
},
}),
[currentQuery],
);
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
const { queryData: paginationQueryData } = useUrlQueryData<Pagination>(
QueryParams.pagination,
);
const queryPayload = useMemo(
() =>
getHostTracesQueryPayload(
timeRange.startTime,
timeRange.endTime,
paginationQueryData?.offset || offset,
tracesFilters,
),
[
timeRange.startTime,
timeRange.endTime,
offset,
tracesFilters,
paginationQueryData,
],
);
const { data, isLoading, isFetching, isError } = useQuery({
queryKey: [
'hostMetricTraces',
timeRange.startTime,
timeRange.endTime,
offset,
tracesFilters,
DEFAULT_ENTITY_VERSION,
paginationQueryData,
],
queryFn: () => GetMetricQueryRange(queryPayload, DEFAULT_ENTITY_VERSION),
enabled: !!queryPayload,
});
const traceListColumns = getListColumns(selectedColumns);
useEffect(() => {
if (data?.payload?.data?.newResult?.data?.result) {
const currentData = data.payload.data.newResult.data.result;
if (currentData.length > 0 && currentData[0].list) {
if (offset === 0) {
setTraces(currentData[0].list ?? []);
} else {
setTraces((prev) => [...prev, ...(currentData[0].list ?? [])]);
}
}
}
}, [data, offset]);
const isDataEmpty =
!isLoading && !isFetching && !isError && traces.length === 0;
const hasAdditionalFilters = tracesFilters.items.length > 1;
const totalCount =
data?.payload?.data?.newResult?.data?.result?.[0]?.list?.length || 0;
return (
<div className="host-metric-traces">
<div className="host-metric-traces-header">
<div className="filter-section">
{query && (
<QueryBuilderSearch
query={query}
onChange={handleChangeTracesFilters}
disableNavigationShortcuts
/>
)}
</div>
<div className="datetime-section">
<DateTimeSelectionV2
showAutoRefresh={false}
showRefreshText={false}
hideShareModal
isModalTimeSelection={isModalTimeSelection}
onTimeChange={handleTimeChange}
defaultRelativeTime="5m"
modalSelectedInterval={selectedInterval}
/>
</div>
</div>
{isError && <ErrorText>{data?.error || 'Something went wrong'}</ErrorText>}
{isLoading && traces.length === 0 && <TracesLoading />}
{isDataEmpty && !hasAdditionalFilters && (
<NoLogs dataSource={DataSource.TRACES} />
)}
{isDataEmpty && hasAdditionalFilters && (
<EmptyLogsSearch dataSource={DataSource.TRACES} panelType="LIST" />
)}
{!isError && traces.length > 0 && (
<div className="host-metric-traces-table">
<TraceExplorerControls
isLoading={isFetching}
totalCount={totalCount}
perPageOptions={PER_PAGE_OPTIONS}
showSizeChanger={false}
/>
<ResizeTable
tableLayout="fixed"
pagination={false}
scroll={{ x: true }}
loading={isFetching}
dataSource={traces}
columns={traceListColumns}
/>
</div>
)}
</div>
);
}
export default HostMetricTraces;

View File

@ -0,0 +1,200 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { nanoToMilli } from 'utils/timeUtils';
export const columns = [
{
dataIndex: 'timestamp',
key: 'timestamp',
title: 'Timestamp',
width: 200,
render: (timestamp: string): string => new Date(timestamp).toLocaleString(),
},
{
title: 'Service Name',
dataIndex: ['data', 'serviceName'],
key: 'serviceName-string-tag',
width: 150,
},
{
title: 'Name',
dataIndex: ['data', 'name'],
key: 'name-string-tag',
width: 145,
},
{
title: 'Duration',
dataIndex: ['data', 'durationNano'],
key: 'durationNano-float64-tag',
width: 145,
render: (duration: number): string => `${nanoToMilli(duration)}ms`,
},
{
title: 'HTTP Method',
dataIndex: ['data', 'httpMethod'],
key: 'httpMethod-string-tag',
width: 145,
},
{
title: 'Status Code',
dataIndex: ['data', 'responseStatusCode'],
key: 'responseStatusCode-string-tag',
width: 145,
},
];
export const selectedColumns: BaseAutocompleteData[] = [
{
key: 'timestamp',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
},
{
key: 'serviceName',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
},
{
key: 'name',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
},
{
key: 'durationNano',
dataType: DataTypes.Float64,
type: 'tag',
isColumn: true,
},
{
key: 'httpMethod',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
},
{
key: 'responseStatusCode',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
},
];
export const getHostTracesQueryPayload = (
start: number,
end: number,
offset = 0,
filters: IBuilderQuery['filters'],
): GetQueryResultsProps => ({
query: {
promql: [],
clickhouse_sql: [],
builder: {
queryData: [
{
dataSource: DataSource.TRACES,
queryName: 'A',
aggregateOperator: 'noop',
aggregateAttribute: {
id: '------false',
dataType: DataTypes.EMPTY,
key: '',
isColumn: false,
type: '',
isJSON: false,
},
timeAggregation: 'rate',
spaceAggregation: 'sum',
functions: [],
filters,
expression: 'A',
disabled: false,
stepInterval: 60,
having: [],
limit: null,
orderBy: [
{
columnName: 'timestamp',
order: 'desc',
},
],
groupBy: [],
legend: '',
reduceTo: 'avg',
},
],
queryFormulas: [],
},
id: '572f1d91-6ac0-46c0-b726-c21488b34434',
queryType: EQueryType.QUERY_BUILDER,
},
graphType: PANEL_TYPES.LIST,
selectedTime: 'GLOBAL_TIME',
start,
end,
params: {
dataSource: DataSource.TRACES,
},
tableParams: {
pagination: {
limit: 10,
offset,
},
selectColumns: [
{
key: 'serviceName',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'serviceName--string--tag--true',
isIndexed: false,
},
{
key: 'name',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'name--string--tag--true',
isIndexed: false,
},
{
key: 'durationNano',
dataType: 'float64',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'durationNano--float64--tag--true',
isIndexed: false,
},
{
key: 'httpMethod',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'httpMethod--string--tag--true',
isIndexed: false,
},
{
key: 'responseStatusCode',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'responseStatusCode--string--tag--true',
isIndexed: false,
},
],
},
});

View File

@ -0,0 +1,84 @@
import { Tag, Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
import {
BlockLink,
getTraceLink,
} from 'container/TracesExplorer/ListView/utils';
import dayjs from 'dayjs';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
const keyToLabelMap: Record<string, string> = {
timestamp: 'Timestamp',
serviceName: 'Service Name',
name: 'Name',
durationNano: 'Duration',
httpMethod: 'HTTP Method',
responseStatusCode: 'Status Code',
};
export const getListColumns = (
selectedColumns: BaseAutocompleteData[],
): ColumnsType<RowData> => {
const columns: ColumnsType<RowData> =
selectedColumns.map(({ dataType, key, type }) => ({
title: keyToLabelMap[key],
dataIndex: key,
key: `${key}-${dataType}-${type}`,
width: 145,
render: (value, item): JSX.Element => {
const itemData = item.data as any;
if (key === 'timestamp') {
const date =
typeof value === 'string'
? dayjs(value).format('YYYY-MM-DD HH:mm:ss.SSS')
: dayjs(value / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS');
return (
<BlockLink to={getTraceLink(item)} openInNewTab>
<Typography.Text>{date}</Typography.Text>
</BlockLink>
);
}
if (value === '') {
return (
<BlockLink to={getTraceLink(itemData)} openInNewTab>
<Typography data-testid={key}>N/A</Typography>
</BlockLink>
);
}
if (key === 'httpMethod' || key === 'responseStatusCode') {
return (
<BlockLink to={getTraceLink(itemData)} openInNewTab>
<Tag data-testid={key} color="magenta">
{itemData[key]}
</Tag>
</BlockLink>
);
}
if (key === 'durationNano') {
const durationNano = itemData[key];
return (
<BlockLink to={getTraceLink(item)} openInNewTab>
<Typography data-testid={key}>{getMs(durationNano)}ms</Typography>
</BlockLink>
);
}
return (
<BlockLink to={getTraceLink(itemData)} openInNewTab>
<Typography data-testid={key}>{itemData[key]}</Typography>
</BlockLink>
);
},
responsive: ['md'],
})) || [];
return columns;
};

View File

@ -0,0 +1,232 @@
.host-detail-drawer {
border-left: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2);
.ant-drawer-header {
padding: 8px 16px;
border-bottom: none;
align-items: stretch;
border-bottom: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
}
.ant-drawer-close {
margin-inline-end: 0px;
}
.ant-drawer-body {
display: flex;
flex-direction: column;
padding: 16px;
}
.title {
color: var(--text-vanilla-400);
font-family: 'Geist Mono';
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.radio-button {
display: flex;
align-items: center;
justify-content: center;
padding-top: var(--padding-1);
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
.host-detail-drawer__host {
.host-details-grid {
.labels-row,
.values-row {
display: grid;
grid-template-columns: 1fr 1.5fr 1.5fr 1.5fr;
gap: 30px;
align-items: center;
}
.labels-row {
margin-bottom: 8px;
}
.host-details-metadata-label {
color: var(--text-vanilla-400);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 18px; /* 163.636% */
letter-spacing: 0.44px;
text-transform: uppercase;
}
.status-tag {
margin: 0;
&.active {
color: var(--success-500);
background: var(--success-100);
border-color: var(--success-500);
}
&.inactive {
color: var(--error-500);
background: var(--error-100);
border-color: var(--error-500);
}
}
.progress-container {
width: 158px;
.ant-progress {
margin: 0;
.ant-progress-text {
font-weight: 600;
}
}
}
.ant-card {
&.ant-card-bordered {
border: 1px solid var(--bg-slate-500) !important;
}
}
}
}
.tabs-and-search {
display: flex;
justify-content: space-between;
align-items: center;
margin: 16px 0;
.action-btn {
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: center;
}
}
.views-tabs-container {
margin-top: 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
.views-tabs {
color: var(--text-vanilla-400);
.view-title {
display: flex;
gap: var(--margin-2);
align-items: center;
justify-content: center;
font-size: var(--font-size-xs);
font-style: normal;
font-weight: var(--font-weight-normal);
}
.tab {
border: 1px solid var(--bg-slate-400);
width: 114px;
}
.tab::before {
background: var(--bg-slate-400);
}
.selected_view {
background: var(--bg-slate-300);
color: var(--text-vanilla-100);
border: 1px solid var(--bg-slate-400);
}
.selected_view::before {
background: var(--bg-slate-400);
}
}
.compass-button {
width: 30px;
height: 30px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
}
.ant-drawer-close {
padding: 0px;
}
}
.lightMode {
.ant-drawer-header {
border-bottom: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-100);
}
.host-detail-drawer {
.title {
color: var(--text-ink-300);
}
.host-detail-drawer__host {
.ant-typography {
color: var(--text-ink-300);
background: transparent;
}
}
.radio-button {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-100);
color: var(--text-ink-300);
}
.views-tabs {
.tab {
background: var(--bg-vanilla-100);
}
.selected_view {
background: var(--bg-vanilla-300);
border: 1px solid var(--bg-slate-300);
color: var(--text-ink-400);
}
.selected_view::before {
background: var(--bg-vanilla-300);
border-left: 1px solid var(--bg-slate-300);
}
}
.compass-button {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
.tabs-and-search {
.action-btn {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-100);
color: var(--text-ink-300);
}
}
}
}

View File

@ -0,0 +1,486 @@
import './HostMetricsDetail.styles.scss';
import { Color, Spacing } from '@signozhq/design-tokens';
import {
Button,
Divider,
Drawer,
Progress,
Radio,
Tag,
Typography,
} from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import { QueryParams } from 'constants/query';
import {
initialQueryBuilderFormValuesMap,
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import GetMinMax from 'lib/getMinMax';
import {
BarChart2,
ChevronsLeftRight,
Compass,
DraftingCompass,
Package2,
ScrollText,
X,
} from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderQuery,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import {
LogsAggregatorOperator,
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as uuidv4 } from 'uuid';
import { VIEW_TYPES, VIEWS } from './constants';
import Containers from './Containers/Containers';
import { HostDetailProps } from './HostMetricDetail.interfaces';
import HostMetricLogsDetailedView from './HostMetricsLogs/HostMetricLogsDetailedView';
import HostMetricTraces from './HostMetricTraces/HostMetricTraces';
import Metrics from './Metrics/Metrics';
import Processes from './Processes/Processes';
// eslint-disable-next-line sonarjs/cognitive-complexity
function HostMetricsDetails({
host,
onClose,
isModalTimeSelection,
}: HostDetailProps): JSX.Element {
const { maxTime, minTime, selectedTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const startMs = useMemo(() => Math.floor(Number(minTime) / 1000000000), [
minTime,
]);
const endMs = useMemo(() => Math.floor(Number(maxTime) / 1000000000), [
maxTime,
]);
const urlQuery = useUrlQuery();
const [modalTimeRange, setModalTimeRange] = useState(() => ({
startTime: startMs,
endTime: endMs,
}));
const [selectedInterval, setSelectedInterval] = useState<Time>(
selectedTime as Time,
);
const [selectedView, setSelectedView] = useState<VIEWS>(VIEWS.METRICS);
const isDarkMode = useIsDarkMode();
const initialFilters = useMemo(
() => ({
op: 'AND',
items: [
{
id: uuidv4(),
key: {
key: 'host.name',
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
id: 'host.name--string--resource--false',
},
op: '=',
value: host?.hostName || '',
},
],
}),
[host?.hostName],
);
const [logFilters, setLogFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
const [tracesFilters, setTracesFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
useEffect(() => {
setLogFilters(initialFilters);
setTracesFilters(initialFilters);
}, [initialFilters]);
useEffect(() => {
setSelectedInterval(selectedTime as Time);
if (selectedTime !== 'custom') {
const { maxTime, minTime } = GetMinMax(selectedTime);
setModalTimeRange({
startTime: Math.floor(minTime / 1000000000),
endTime: Math.floor(maxTime / 1000000000),
});
}
}, [selectedTime, minTime, maxTime]);
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
};
const handleTimeChange = useCallback(
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
setSelectedInterval(interval as Time);
if (interval === 'custom' && dateTimeRange) {
setModalTimeRange({
startTime: Math.floor(dateTimeRange[0] / 1000),
endTime: Math.floor(dateTimeRange[1] / 1000),
});
} else {
const { maxTime, minTime } = GetMinMax(interval);
setModalTimeRange({
startTime: Math.floor(minTime / 1000000000),
endTime: Math.floor(maxTime / 1000000000),
});
}
},
[],
);
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setLogFilters((prevFilters) => {
const hostNameFilter = prevFilters.items.find(
(item) => item.key?.key === 'host.name',
);
const paginationFilter = value.items.find((item) => item.key?.key === 'id');
const newFilters = value.items.filter(
(item) => item.key?.key !== 'id' && item.key?.key !== 'host.name',
);
return {
op: 'AND',
items: [
hostNameFilter,
...newFilters,
...(paginationFilter ? [paginationFilter] : []),
].filter((item): item is TagFilterItem => item !== undefined),
};
});
},
[],
);
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setTracesFilters((prevFilters) => {
const hostNameFilter = prevFilters.items.find(
(item) => item.key?.key === 'host.name',
);
return {
op: 'AND',
items: [
hostNameFilter,
...value.items.filter((item) => item.key?.key !== 'host.name'),
].filter((item): item is TagFilterItem => item !== undefined),
};
});
},
[],
);
const handleExplorePagesRedirect = (): void => {
if (selectedInterval !== 'custom') {
urlQuery.set(QueryParams.relativeTime, selectedInterval);
} else {
urlQuery.delete(QueryParams.relativeTime);
urlQuery.set(QueryParams.startTime, modalTimeRange.startTime.toString());
urlQuery.set(QueryParams.endTime, modalTimeRange.endTime.toString());
}
if (selectedView === VIEW_TYPES.LOGS) {
const filtersWithoutPagination = {
...logFilters,
items: logFilters.items.filter((item) => item.key?.key !== 'id'),
};
const compositeQuery = {
...initialQueryState,
queryType: 'builder',
builder: {
...initialQueryState.builder,
queryData: [
{
...initialQueryBuilderFormValuesMap.logs,
aggregateOperator: LogsAggregatorOperator.NOOP,
filters: filtersWithoutPagination,
},
],
},
};
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
queryType: 'builder',
builder: {
...initialQueryState.builder,
queryData: [
{
...initialQueryBuilderFormValuesMap.traces,
aggregateOperator: TracesAggregatorOperator.NOOP,
filters: tracesFilters,
},
],
},
};
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
}
};
const handleClose = (): void => {
setSelectedInterval(selectedTime as Time);
if (selectedTime !== 'custom') {
const { maxTime, minTime } = GetMinMax(selectedTime);
setModalTimeRange({
startTime: Math.floor(minTime / 1000000000),
endTime: Math.floor(maxTime / 1000000000),
});
}
setSelectedView(VIEW_TYPES.METRICS);
onClose();
};
return (
<Drawer
width="70%"
title={
<>
<Divider type="vertical" />
<Typography.Text className="title">{host?.hostName}</Typography.Text>
</>
}
placement="right"
onClose={handleClose}
open={!!host}
style={{
overscrollBehavior: 'contain',
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
}}
className="host-detail-drawer"
destroyOnClose
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
>
{host && (
<>
<div className="host-detail-drawer__host">
<div className="host-details-grid">
<div className="labels-row">
<Typography.Text
type="secondary"
className="host-details-metadata-label"
>
STATUS
</Typography.Text>
<Typography.Text
type="secondary"
className="host-details-metadata-label"
>
OPERATING SYSTEM
</Typography.Text>
<Typography.Text
type="secondary"
className="host-details-metadata-label"
>
CPU USAGE
</Typography.Text>
<Typography.Text
type="secondary"
className="host-details-metadata-label"
>
MEMORY USAGE
</Typography.Text>
</div>
<div className="values-row">
<Tag
bordered
className={`infra-monitoring-tags ${
host.active ? 'active' : 'inactive'
}`}
>
{host.active ? 'ACTIVE' : 'INACTIVE'}
</Tag>
<Tag className="infra-monitoring-tags" bordered>
{host.os}
</Tag>
<div className="progress-container">
<Progress
percent={Number((host.cpu * 100).toFixed(1))}
size="small"
strokeColor={((): string => {
const cpuPercent = Number((host.cpu * 100).toFixed(1));
if (cpuPercent >= 90) return Color.BG_SAKURA_500;
if (cpuPercent >= 60) return Color.BG_AMBER_500;
return Color.BG_FOREST_500;
})()}
className="progress-bar"
/>
</div>
<div className="progress-container">
<Progress
percent={Number((host.memory * 100).toFixed(1))}
size="small"
strokeColor={((): string => {
const memoryPercent = Number((host.memory * 100).toFixed(1));
if (memoryPercent >= 90) return Color.BG_CHERRY_500;
if (memoryPercent >= 60) return Color.BG_AMBER_500;
return Color.BG_FOREST_500;
})()}
className="progress-bar"
/>
</div>
</div>
</div>
</div>
<div className="views-tabs-container">
<Radio.Group
className="views-tabs"
onChange={handleTabChange}
value={selectedView}
>
<Radio.Button
className={
// eslint-disable-next-line sonarjs/no-duplicate-string
selectedView === VIEW_TYPES.METRICS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.METRICS}
>
<div className="view-title">
<BarChart2 size={14} />
Metrics
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.LOGS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.LOGS}
>
<div className="view-title">
<ScrollText size={14} />
Logs
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.TRACES ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.TRACES}
>
<div className="view-title">
<DraftingCompass size={14} />
Traces
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.CONTAINERS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.CONTAINERS}
>
<div className="view-title">
<Package2 size={14} />
Containers
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.PROCESSES ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.PROCESSES}
>
<div className="view-title">
<ChevronsLeftRight size={14} />
Processes
</div>
</Radio.Button>
</Radio.Group>
{(selectedView === VIEW_TYPES.LOGS ||
selectedView === VIEW_TYPES.TRACES) && (
<Button
icon={<Compass size={18} />}
className="compass-button"
onClick={handleExplorePagesRedirect}
/>
)}
</div>
{selectedView === VIEW_TYPES.METRICS && (
<Metrics
selectedInterval={selectedInterval}
hostName={host.hostName}
timeRange={modalTimeRange}
handleTimeChange={handleTimeChange}
isModalTimeSelection={isModalTimeSelection}
/>
)}
{selectedView === VIEW_TYPES.LOGS && (
<HostMetricLogsDetailedView
timeRange={modalTimeRange}
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeLogFilters={handleChangeLogFilters}
logFilters={logFilters}
selectedInterval={selectedInterval}
/>
)}
{selectedView === VIEW_TYPES.TRACES && (
<HostMetricTraces
timeRange={modalTimeRange}
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeTracesFilters={handleChangeTracesFilters}
tracesFilters={tracesFilters}
selectedInterval={selectedInterval}
/>
)}
{selectedView === VIEW_TYPES.CONTAINERS && <Containers />}
{selectedView === VIEW_TYPES.PROCESSES && <Processes />}
</>
)}
</Drawer>
);
}
export default HostMetricsDetails;

View File

@ -0,0 +1,133 @@
.host-metrics-logs-container {
margin-top: 1rem;
.filter-section {
flex: 1;
.ant-select-selector {
border-radius: 2px;
border: 1px solid var(--bg-slate-400) !important;
background-color: var(--bg-ink-300) !important;
input {
font-size: 12px;
}
.ant-tag .ant-typography {
font-size: 12px;
}
}
}
.host-metrics-logs-header {
display: flex;
justify-content: space-between;
gap: 8px;
padding: 12px;
border-radius: 3px;
border: 1px solid var(--bg-slate-500);
}
.host-metrics-logs {
margin-top: 1rem;
.virtuoso-list {
overflow-y: hidden !important;
&::-webkit-scrollbar {
width: 0.3rem;
height: 0.3rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--bg-slate-300);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-slate-200);
}
.ant-row {
width: fit-content;
}
}
.skeleton-container {
height: 100%;
padding: 16px;
}
}
}
.host-metrics-logs-list-container {
flex: 1;
height: calc(100vh - 272px) !important;
display: flex;
height: 100%;
.raw-log-content {
width: 100%;
text-wrap: inherit;
word-wrap: break-word;
}
}
.host-metrics-logs-list-card {
width: 100%;
margin-top: 12px;
.ant-card-body {
padding: 0;
height: 100%;
width: 100%;
}
}
.logs-loading-skeleton {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
padding: 8px 0;
.ant-skeleton-input-sm {
height: 18px;
}
}
.no-logs-found {
height: 50vh;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: 24px;
box-sizing: border-box;
.ant-typography {
display: flex;
align-items: center;
gap: 16px;
}
}
.lightMode {
.filter-section {
border-top: 1px solid var(--bg-vanilla-300);
border-bottom: 1px solid var(--bg-vanilla-300);
.ant-select-selector {
border-color: var(--bg-vanilla-300) !important;
background-color: var(--bg-vanilla-100) !important;
color: var(--bg-ink-200);
}
}
}

View File

@ -0,0 +1,95 @@
import './HostMetricLogs.styles.scss';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useMemo } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import HostMetricsLogs from './HostMetricsLogs';
interface Props {
timeRange: {
startTime: number;
endTime: number;
};
isModalTimeSelection: boolean;
handleTimeChange: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
handleChangeLogFilters: (value: IBuilderQuery['filters']) => void;
logFilters: IBuilderQuery['filters'];
selectedInterval: Time;
}
function HostMetricLogsDetailedView({
timeRange,
isModalTimeSelection,
handleTimeChange,
handleChangeLogFilters,
logFilters,
selectedInterval,
}: Props): JSX.Element {
const { currentQuery } = useQueryBuilder();
const updatedCurrentQuery = useMemo(
() => ({
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
{
...currentQuery.builder.queryData[0],
dataSource: DataSource.LOGS,
aggregateOperator: 'noop',
aggregateAttribute: {
...currentQuery.builder.queryData[0].aggregateAttribute,
},
},
],
},
}),
[currentQuery],
);
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
return (
<div className="host-metrics-logs-container">
<div className="host-metrics-logs-header">
<div className="filter-section">
{query && (
<QueryBuilderSearch
query={query}
onChange={handleChangeLogFilters}
disableNavigationShortcuts
/>
)}
</div>
<div className="datetime-section">
<DateTimeSelectionV2
showAutoRefresh={false}
showRefreshText={false}
hideShareModal
isModalTimeSelection={isModalTimeSelection}
onTimeChange={handleTimeChange}
defaultRelativeTime="5m"
modalSelectedInterval={selectedInterval}
/>
</div>
</div>
<HostMetricsLogs
timeRange={timeRange}
handleChangeLogFilters={handleChangeLogFilters}
filters={logFilters}
/>
</div>
);
}
export default HostMetricLogsDetailedView;

View File

@ -0,0 +1,216 @@
/* eslint-disable no-nested-ternary */
import './HostMetricLogs.styles.scss';
import { Card } from 'antd';
import RawLogView from 'components/Logs/RawLogView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import LogsError from 'container/LogsError/LogsError';
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import { FontSize } from 'container/OptionsMenu/types';
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { isEqual } from 'lodash-es';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { Virtuoso } from 'react-virtuoso';
import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderQuery,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import { v4 } from 'uuid';
import { getHostLogsQueryPayload } from './constants';
import NoLogsContainer from './NoLogsContainer';
interface Props {
timeRange: {
startTime: number;
endTime: number;
};
handleChangeLogFilters: (filters: IBuilderQuery['filters']) => void;
filters: IBuilderQuery['filters'];
}
function HostMetricsLogs({
timeRange,
handleChangeLogFilters,
filters,
}: Props): JSX.Element {
const [logs, setLogs] = useState<ILog[]>([]);
const [hasReachedEndOfLogs, setHasReachedEndOfLogs] = useState(false);
const [restFilters, setRestFilters] = useState<TagFilterItem[]>([]);
const [resetLogsList, setResetLogsList] = useState<boolean>(false);
useEffect(() => {
const newRestFilters = filters.items.filter(
(item) => item.key?.key !== 'id' && item.key?.key !== 'host.name',
);
const areFiltersSame = isEqual(restFilters, newRestFilters);
if (!areFiltersSame) {
setResetLogsList(true);
}
setRestFilters(newRestFilters);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filters]);
const queryPayload = useMemo(() => {
const basePayload = getHostLogsQueryPayload(
timeRange.startTime,
timeRange.endTime,
filters,
);
basePayload.query.builder.queryData[0].pageSize = 100;
basePayload.query.builder.queryData[0].orderBy = [
{ columnName: 'timestamp', order: ORDERBY_FILTERS.DESC },
];
return basePayload;
}, [timeRange.startTime, timeRange.endTime, filters]);
const [isPaginating, setIsPaginating] = useState(false);
const { data, isLoading, isFetching, isError } = useQuery({
queryKey: [
'hostMetricsLogs',
timeRange.startTime,
timeRange.endTime,
filters,
],
queryFn: () => GetMetricQueryRange(queryPayload, DEFAULT_ENTITY_VERSION),
enabled: !!queryPayload,
keepPreviousData: isPaginating,
});
useEffect(() => {
if (data?.payload?.data?.newResult?.data?.result) {
const currentData = data.payload.data.newResult.data.result;
if (resetLogsList) {
const currentLogs: ILog[] =
currentData[0].list?.map((item) => ({
...item.data,
timestamp: item.timestamp,
})) || [];
setLogs(currentLogs);
setResetLogsList(false);
}
if (currentData.length > 0 && currentData[0].list) {
const currentLogs: ILog[] =
currentData[0].list.map((item) => ({
...item.data,
timestamp: item.timestamp,
})) || [];
setLogs((prev) => [...prev, ...currentLogs]);
} else {
setHasReachedEndOfLogs(true);
}
}
}, [data, restFilters, isPaginating, resetLogsList]);
const getItemContent = useCallback(
(_: number, logToRender: ILog): JSX.Element => (
<RawLogView
isReadOnly
isTextOverflowEllipsisDisabled
key={logToRender.id}
data={logToRender}
linesPerRow={5}
fontSize={FontSize.MEDIUM}
/>
),
[],
);
const loadMoreLogs = useCallback(() => {
if (!logs.length) return;
setIsPaginating(true);
const lastLog = logs[logs.length - 1];
const newItems = [
...filters.items.filter((item) => item.key?.key !== 'id'),
{
id: v4(),
key: {
key: 'id',
type: '',
dataType: DataTypes.String,
isColumn: true,
},
op: '<',
value: lastLog.id,
},
];
const newFilters = {
op: 'AND',
items: newItems,
} as IBuilderQuery['filters'];
handleChangeLogFilters(newFilters);
}, [logs, filters, handleChangeLogFilters]);
useEffect(() => {
setIsPaginating(false);
}, [data]);
const renderFooter = useCallback(
(): JSX.Element | null => (
// eslint-disable-next-line react/jsx-no-useless-fragment
<>
{isFetching ? (
<div className="logs-loading-skeleton"> Loading more logs ... </div>
) : hasReachedEndOfLogs ? (
<div className="logs-loading-skeleton"> *** End *** </div>
) : null}
</>
),
[isFetching, hasReachedEndOfLogs],
);
const renderContent = useMemo(
() => (
<Card bordered={false} className="host-metrics-logs-list-card">
<OverlayScrollbar isVirtuoso>
<Virtuoso
className="host-metrics-logs-virtuoso"
key="host-metrics-logs-virtuoso"
data={logs}
endReached={loadMoreLogs}
totalCount={logs.length}
itemContent={getItemContent}
overscan={200}
components={{
Footer: renderFooter,
}}
/>
</OverlayScrollbar>
</Card>
),
[logs, loadMoreLogs, getItemContent, renderFooter],
);
return (
<div className="host-metrics-logs">
{isLoading && <LogsLoading />}
{!isLoading && !isError && logs.length === 0 && <NoLogsContainer />}
{isError && !isLoading && <LogsError />}
{!isLoading && !isError && logs.length > 0 && (
<div className="host-metrics-logs-list-container">{renderContent}</div>
)}
</div>
);
}
export default HostMetricsLogs;

View File

@ -0,0 +1,16 @@
import { Color } from '@signozhq/design-tokens';
import { Typography } from 'antd';
import { Ghost } from 'lucide-react';
const { Text } = Typography;
export default function NoLogsContainer(): React.ReactElement {
return (
<div className="no-logs-found">
<Text type="secondary">
<Ghost size={24} color={Color.BG_AMBER_500} /> No logs found for this host
in the selected time range.
</Text>
</div>
);
}

View File

@ -0,0 +1,65 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { v4 as uuidv4 } from 'uuid';
export const getHostLogsQueryPayload = (
start: number,
end: number,
filters: IBuilderQuery['filters'],
): GetQueryResultsProps => ({
graphType: PANEL_TYPES.LIST,
selectedTime: 'GLOBAL_TIME',
query: {
clickhouse_sql: [],
promql: [],
builder: {
queryData: [
{
dataSource: DataSource.LOGS,
queryName: 'A',
aggregateOperator: 'noop',
aggregateAttribute: {
id: '------false',
dataType: DataTypes.String,
key: '',
isColumn: false,
type: '',
isJSON: false,
},
timeAggregation: 'rate',
spaceAggregation: 'sum',
functions: [],
filters,
expression: 'A',
disabled: false,
stepInterval: 60,
having: [],
limit: null,
orderBy: [
{
columnName: 'timestamp',
order: 'desc',
},
],
groupBy: [],
legend: '',
reduceTo: 'avg',
offset: 0,
pageSize: 100,
},
],
queryFormulas: [],
},
id: uuidv4(),
queryType: EQueryType.QUERY_BUILDER,
},
params: {
lastLogLineTimestamp: null,
},
start,
end,
});

View File

@ -0,0 +1,45 @@
.empty-container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.host-metrics-container {
margin-top: 1rem;
}
.metrics-header {
display: flex;
justify-content: flex-end;
margin-top: 1rem;
gap: 8px;
padding: 12px;
border-radius: 3px;
border: 1px solid var(--bg-slate-500);
}
.host-metrics-card {
margin: 8px 0 1rem 0;
height: 300px;
padding: 10px;
border: 1px solid var(--bg-slate-500);
.ant-card-body {
padding: 0;
}
.chart-container {
width: 100%;
height: 100%;
}
.no-data-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}

View File

@ -0,0 +1,142 @@
import './Metrics.styles.scss';
import { Card, Col, Row, Skeleton, Typography } from 'antd';
import cx from 'classnames';
import Uplot from 'components/Uplot';
import { ENTITY_VERSION_V4 } from 'constants/app';
import {
getHostQueryPayload,
hostWidgetInfo,
} from 'container/LogDetailedView/InfraMetrics/constants';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useMemo, useRef } from 'react';
import { useQueries, UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
interface MetricsTabProps {
timeRange: {
startTime: number;
endTime: number;
};
isModalTimeSelection: boolean;
handleTimeChange: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
selectedInterval: Time;
hostName: string;
}
function Metrics({
selectedInterval,
hostName,
timeRange,
handleTimeChange,
isModalTimeSelection,
}: MetricsTabProps): JSX.Element {
const queryPayloads = useMemo(
() => getHostQueryPayload(hostName, timeRange.startTime, timeRange.endTime),
[hostName, timeRange.startTime, timeRange.endTime],
);
const queries = useQueries(
queryPayloads.map((payload) => ({
queryKey: ['host-metrics', payload, ENTITY_VERSION_V4, 'HOST'],
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
enabled: !!payload,
})),
);
const isDarkMode = useIsDarkMode();
const graphRef = useRef<HTMLDivElement>(null);
const dimensions = useResizeObserver(graphRef);
const chartData = useMemo(
() => queries.map(({ data }) => getUPlotChartData(data?.payload)),
[queries],
);
const options = useMemo(
() =>
queries.map(({ data }, idx) =>
getUPlotChartOptions({
apiResponse: data?.payload,
isDarkMode,
dimensions,
yAxisUnit: hostWidgetInfo[idx].yAxisUnit,
softMax: null,
softMin: null,
minTimeScale: timeRange.startTime,
maxTimeScale: timeRange.endTime,
}),
),
[queries, isDarkMode, dimensions, timeRange.startTime, timeRange.endTime],
);
const renderCardContent = (
query: UseQueryResult<SuccessResponse<MetricRangePayloadProps>, unknown>,
idx: number,
): JSX.Element => {
if (query.isLoading) {
return <Skeleton />;
}
if (query.error) {
const errorMessage =
(query.error as Error)?.message || 'Something went wrong';
return <div>{errorMessage}</div>;
}
return (
<div
className={cx('chart-container', {
'no-data-container':
!query.isLoading && !query?.data?.payload?.data?.result?.length,
})}
>
<Uplot options={options[idx]} data={chartData[idx]} />
</div>
);
};
return (
<>
<div className="metrics-header">
<div className="metrics-datetime-section">
<DateTimeSelectionV2
showAutoRefresh={false}
showRefreshText={false}
hideShareModal
onTimeChange={handleTimeChange}
defaultRelativeTime="5m"
isModalTimeSelection={isModalTimeSelection}
modalSelectedInterval={selectedInterval}
/>
</div>
</div>
<Row gutter={24} className="host-metrics-container">
{queries.map((query, idx) => (
<Col span={12} key={hostWidgetInfo[idx].title}>
<Typography.Text>{hostWidgetInfo[idx].title}</Typography.Text>
<Card bordered className="host-metrics-card" ref={graphRef}>
{renderCardContent(query, idx)}
</Card>
</Col>
))}
</Row>
</>
);
}
export default Metrics;

View File

@ -0,0 +1,48 @@
.host-processes {
max-width: 600px;
margin: 150px auto;
padding: 0 16px;
.infra-container-card {
display: flex;
flex-direction: column;
justify-content: center;
}
.infra-container-card-text {
font-size: var(--font-size-sm);
color: var(--text-vanilla-400);
line-height: 20px;
letter-spacing: -0.07px;
width: 400px;
font-family: 'Inter';
margin-top: 12px;
}
.infra-container-working-msg {
display: flex;
width: 400px;
padding: 12px;
align-items: flex-start;
gap: 12px;
border-radius: 4px;
background: rgba(171, 189, 255, 0.04);
.ant-space {
align-items: flex-start;
}
}
.infra-container-contact-support-btn {
display: flex;
align-items: center;
justify-content: center;
margin: auto;
}
}
.lightMode {
.infra-container-card-text {
color: var(--text-ink-200);
}
}

View File

@ -0,0 +1,35 @@
import './Processes.styles.scss';
import { Space, Typography } from 'antd';
import { useTranslation } from 'react-i18next';
const { Text } = Typography;
function Processes(): JSX.Element {
const { t } = useTranslation(['infraMonitoring']);
return (
<Space direction="vertical" className="host-processes" size={24}>
<div className="infra-container-card">
<img
src="/Icons/infraContainers.svg"
alt="infra-container"
width={32}
height={32}
/>
<Text className="infra-container-card-text">
{t('processes_visualization_message')}
</Text>
</div>
<div className="infra-container-working-msg">
<Space>
<img src="/Icons/broom.svg" alt="broom" width={16} height={16} />
<Text className="infra-container-card-text">{t('working_message')}</Text>
</Space>
</div>
</Space>
);
}
export default Processes;

View File

@ -0,0 +1,15 @@
export enum VIEWS {
METRICS = 'metrics',
LOGS = 'logs',
TRACES = 'traces',
CONTAINERS = 'containers',
PROCESSES = 'processes',
}
export const VIEW_TYPES = {
METRICS: VIEWS.METRICS,
LOGS: VIEWS.LOGS,
TRACES: VIEWS.TRACES,
CONTAINERS: VIEWS.CONTAINERS,
PROCESSES: VIEWS.PROCESSES,
};

View File

@ -0,0 +1,3 @@
import HostMetricsDetails from './HostMetricsDetails';
export default HostMetricsDetails;

View File

@ -173,6 +173,7 @@ function RawLogView({
<LogStateIndicator type={logType} fontSize={fontSize} /> <LogStateIndicator type={logType} fontSize={fontSize} />
<RawLogContent <RawLogContent
className="raw-log-content"
$isReadOnly={isReadOnly} $isReadOnly={isReadOnly}
$isActiveLog={isActiveLog} $isActiveLog={isActiveLog}
$isDarkMode={isDarkMode} $isDarkMode={isDarkMode}

View File

@ -23,4 +23,5 @@ export enum FeatureKeys {
PREMIUM_SUPPORT = 'PREMIUM_SUPPORT', PREMIUM_SUPPORT = 'PREMIUM_SUPPORT',
QUERY_BUILDER_SEARCH_V2 = 'QUERY_BUILDER_SEARCH_V2', QUERY_BUILDER_SEARCH_V2 = 'QUERY_BUILDER_SEARCH_V2',
ANOMALY_DETECTION = 'ANOMALY_DETECTION', ANOMALY_DETECTION = 'ANOMALY_DETECTION',
HOSTS_INFRA_MONITORING = 'HOSTS_INFRA_MONITORING',
} }

View File

@ -18,5 +18,6 @@ export const REACT_QUERY_KEY = {
GET_ALL_ALLERTS: 'GET_ALL_ALLERTS', GET_ALL_ALLERTS: 'GET_ALL_ALLERTS',
REMOVE_ALERT_RULE: 'REMOVE_ALERT_RULE', REMOVE_ALERT_RULE: 'REMOVE_ALERT_RULE',
DUPLICATE_ALERT_RULE: 'DUPLICATE_ALERT_RULE', DUPLICATE_ALERT_RULE: 'DUPLICATE_ALERT_RULE',
GET_HOST_LIST: 'GET_HOST_LIST',
UPDATE_ALERT_RULE: 'UPDATE_ALERT_RULE', UPDATE_ALERT_RULE: 'UPDATE_ALERT_RULE',
}; };

View File

@ -59,6 +59,7 @@ const ROUTES = {
INTEGRATIONS: '/integrations', INTEGRATIONS: '/integrations',
MESSAGING_QUEUES: '/messaging-queues', MESSAGING_QUEUES: '/messaging-queues',
MESSAGING_QUEUES_DETAIL: '/messaging-queues/detail', MESSAGING_QUEUES_DETAIL: '/messaging-queues/detail',
INFRASTRUCTURE_MONITORING_HOSTS: '/infrastructure-monitoring/hosts',
} as const; } as const;
export default ROUTES; export default ROUTES;

View File

@ -240,6 +240,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD'; const isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD';
const isAlertHistory = (): boolean => routeKey === 'ALERT_HISTORY'; const isAlertHistory = (): boolean => routeKey === 'ALERT_HISTORY';
const isAlertOverview = (): boolean => routeKey === 'ALERT_OVERVIEW'; const isAlertOverview = (): boolean => routeKey === 'ALERT_OVERVIEW';
const isInfraMonitoringHosts = (): boolean =>
routeKey === 'INFRASTRUCTURE_MONITORING_HOSTS';
const isPathMatch = (regex: RegExp): boolean => regex.test(pathname); const isPathMatch = (regex: RegExp): boolean => regex.test(pathname);
const isDashboardView = (): boolean => const isDashboardView = (): boolean =>
@ -306,7 +308,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
isDashboardListView() || isDashboardListView() ||
isAlertHistory() || isAlertHistory() ||
isAlertOverview() || isAlertOverview() ||
isMessagingQueues() isMessagingQueues() ||
isInfraMonitoringHosts()
? 0 ? 0
: '0 1rem', : '0 1rem',

View File

@ -17,6 +17,7 @@ function Controls({
handleNavigateNext, handleNavigateNext,
handleCountItemsPerPageChange, handleCountItemsPerPageChange,
isLogPanel = false, isLogPanel = false,
showSizeChanger = true,
}: ControlsProps): JSX.Element | null { }: ControlsProps): JSX.Element | null {
const isNextAndPreviousDisabled = useMemo( const isNextAndPreviousDisabled = useMemo(
() => isLoading || countPerPage < 0 || totalCount === 0, () => isLoading || countPerPage < 0 || totalCount === 0,
@ -52,6 +53,8 @@ function Controls({
> >
Next <RightOutlined /> Next <RightOutlined />
</Button> </Button>
{showSizeChanger && (
<Select<Pagination['limit']> <Select<Pagination['limit']>
style={defaultSelectStyle} style={defaultSelectStyle}
loading={isLoading} loading={isLoading}
@ -66,6 +69,7 @@ function Controls({
>{`${count} / page`}</Select.Option> >{`${count} / page`}</Select.Option>
))} ))}
</Select> </Select>
)}
</Container> </Container>
); );
} }
@ -74,6 +78,7 @@ Controls.defaultProps = {
offset: 0, offset: 0,
perPageOptions: DEFAULT_PER_PAGE_OPTIONS, perPageOptions: DEFAULT_PER_PAGE_OPTIONS,
isLogPanel: false, isLogPanel: false,
showSizeChanger: true,
}; };
export interface ControlsProps { export interface ControlsProps {
@ -86,6 +91,7 @@ export interface ControlsProps {
handleNavigateNext: () => void; handleNavigateNext: () => void;
handleCountItemsPerPageChange: (value: Pagination['limit']) => void; handleCountItemsPerPageChange: (value: Pagination['limit']) => void;
isLogPanel?: boolean; isLogPanel?: boolean;
showSizeChanger?: boolean;
} }
export default memo(Controls); export default memo(Controls);

View File

@ -1,4 +1,4 @@
import { Color, ColorType } from '@signozhq/design-tokens'; import { Color } from '@signozhq/design-tokens';
import { showErrorNotification } from 'components/ExplorerCard/utils'; import { showErrorNotification } from 'components/ExplorerCard/utils';
import { LOCALSTORAGE } from 'constants/localStorage'; import { LOCALSTORAGE } from 'constants/localStorage';
import { QueryParams } from 'constants/query'; import { QueryParams } from 'constants/query';
@ -8,7 +8,7 @@ import { DataSource } from 'types/common/queryBuilder';
import { SaveNewViewHandlerProps } from './types'; import { SaveNewViewHandlerProps } from './types';
export const getRandomColor = (): ColorType => { export const getRandomColor = (): string => {
const colorKeys = Object.keys(Color) as (keyof typeof Color)[]; const colorKeys = Object.keys(Color) as (keyof typeof Color)[];
const randomKey = colorKeys[Math.floor(Math.random() * colorKeys.length)]; const randomKey = colorKeys[Math.floor(Math.random() * colorKeys.length)];
return Color[randomKey]; return Color[randomKey];

View File

@ -0,0 +1,19 @@
.loading-host-metrics {
padding: 24px 0;
height: 600px;
display: flex;
justify-content: center;
align-items: center;
.loading-host-metrics-content {
display: flex;
align-items: center;
flex-direction: column;
.loading-gif {
height: 72px;
margin-left: -24px;
}
}
}

View File

@ -0,0 +1,26 @@
import './HostMetricsLoading.styles.scss';
import { Typography } from 'antd';
import { useTranslation } from 'react-i18next';
import { DataSource } from 'types/common/queryBuilder';
export function HostMetricsLoading(): JSX.Element {
const { t } = useTranslation('common');
return (
<div className="loading-host-metrics">
<div className="loading-host-metrics-content">
<img
className="loading-gif"
src="/Icons/loading-plane.gif"
alt="wait-icon"
/>
<Typography>
{t('pending_data_placeholder', {
dataSource: `host ${DataSource.METRICS}`,
})}
</Typography>
</div>
</div>
);
}

View File

@ -0,0 +1,187 @@
import './InfraMonitoring.styles.scss';
import { LoadingOutlined } from '@ant-design/icons';
import {
Spin,
Table,
TablePaginationConfig,
TableProps,
Typography,
} from 'antd';
import { SorterResult } from 'antd/es/table/interface';
import { HostListPayload } from 'api/infraMonitoring/getHostLists';
import HostMetricDetail from 'components/HostMetricsDetail';
import NoLogs from 'container/NoLogs/NoLogs';
import { useGetHostList } from 'hooks/infraMonitoring/useGetHostList';
import { useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import HostsListControls from './HostsListControls';
import {
formatDataForTable,
getHostListsQuery,
getHostsListColumns,
HostRowData,
} from './utils';
function HostsList(): JSX.Element {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const [currentPage, setCurrentPage] = useState(1);
const [filters, setFilters] = useState<IBuilderQuery['filters']>({
items: [],
op: 'and',
});
const [orderBy, setOrderBy] = useState<{
columnName: string;
order: 'asc' | 'desc';
} | null>(null);
const [selectedHostName, setSelectedHostName] = useState<string | null>(null);
const pageSize = 10;
const query = useMemo(() => {
const baseQuery = getHostListsQuery();
return {
...baseQuery,
limit: pageSize,
offset: (currentPage - 1) * pageSize,
filters,
start: Math.floor(minTime / 1000000),
end: Math.floor(maxTime / 1000000),
orderBy,
};
}, [currentPage, filters, minTime, maxTime, orderBy]);
const { data, isFetching, isLoading, isError } = useGetHostList(
query as HostListPayload,
{
queryKey: ['hostList', query],
enabled: !!query,
},
);
const hostMetricsData = useMemo(() => data?.payload?.data?.records || [], [
data,
]);
const totalCount = data?.payload?.data?.total || 0;
const formattedHostMetricsData = useMemo(
() => formatDataForTable(hostMetricsData),
[hostMetricsData],
);
const columns = useMemo(() => getHostsListColumns(), []);
const isDataPresent =
!isLoading && !isFetching && !isError && hostMetricsData.length === 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);
}
},
[],
);
const handleFiltersChange = useCallback(
(value: IBuilderQuery['filters']): void => {
const isNewFilterAdded = value.items.length !== filters.items.length;
if (isNewFilterAdded) {
setFilters(value);
setCurrentPage(1);
}
},
[filters],
);
const selectedHostData = useMemo(() => {
if (!selectedHostName) return null;
return (
hostMetricsData.find((host) => host.hostName === selectedHostName) || null
);
}, [selectedHostName, hostMetricsData]);
const handleRowClick = (record: HostRowData): void => {
setSelectedHostName(record.hostName);
};
const handleCloseHostDetail = (): void => {
setSelectedHostName(null);
};
return (
<div className="hosts-list">
<HostsListControls handleFiltersChange={handleFiltersChange} />
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
{isDataPresent && filters.items.length === 0 && (
<NoLogs dataSource={DataSource.METRICS} />
)}
{!isFetching &&
!isLoading &&
formattedHostMetricsData.length === 0 &&
filters.items.length > 0 && (
<div className="no-hosts-message">No hosts match the applied filters.</div>
)}
{!isError && (
<Table
className="hosts-list-table"
dataSource={isFetching || isLoading ? [] : formattedHostMetricsData}
columns={columns}
pagination={{
current: currentPage,
pageSize,
total: totalCount,
showSizeChanger: false,
hideOnSinglePage: true,
}}
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',
})}
/>
)}
<HostMetricDetail
host={selectedHostData}
isModalTimeSelection
onClose={handleCloseHostDetail}
/>
</div>
);
}
export default HostsList;

View File

@ -0,0 +1,64 @@
import './InfraMonitoring.styles.scss';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useCallback, useMemo } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
function HostsListControls({
handleFiltersChange,
}: {
handleFiltersChange: (value: IBuilderQuery['filters']) => void;
}): JSX.Element {
const { currentQuery } = useQueryBuilder();
const updatedCurrentQuery = useMemo(
() => ({
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
{
...currentQuery.builder.queryData[0],
aggregateOperator: 'noop',
aggregateAttribute: {
...currentQuery.builder.queryData[0].aggregateAttribute,
},
},
],
},
}),
[currentQuery],
);
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
const handleChangeTagFilters = useCallback(
(value: IBuilderQuery['filters']) => {
handleFiltersChange(value);
},
[handleFiltersChange],
);
return (
<div className="hosts-list-controls">
<div className="hosts-list-controls-left">
<QueryBuilderSearch
query={query}
onChange={handleChangeTagFilters}
isInfraMonitoring
disableNavigationShortcuts
/>
</div>
<div className="time-selector">
<DateTimeSelectionV2
showAutoRefresh={false}
showRefreshText={false}
hideShareModal
/>
</div>
</div>
);
}
export default HostsListControls;

View File

@ -0,0 +1,279 @@
.infra-monitoring-container {
display: flex;
height: 100%;
flex-direction: column;
.infra-monitoring-header {
display: flex;
justify-content: space-between;
width: 100%;
margin-bottom: 16px;
}
.no-hosts-message {
padding: 16px;
}
.hosts-list-controls {
padding: 8px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
.ant-select-selector {
border-radius: 2px;
border: 1px solid var(--bg-slate-400) !important;
background-color: var(--bg-ink-300) !important;
input {
font-size: 12px;
}
.ant-tag .ant-typography {
font-size: 12px;
}
}
.hosts-list-controls-left {
flex: 1;
}
}
.progress-container {
display: flex;
align-items: center;
}
.progress-bar {
flex: 1;
margin-right: 8px;
}
.clickable-row {
cursor: pointer;
}
.hosts-list-table {
.ant-table {
.ant-table-thead > tr > th {
padding: 12px;
font-weight: 500;
font-size: 12px;
line-height: 18px;
background: var(--bg-ink-500);
border-bottom: none;
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 600;
line-height: 18px; /* 163.636% */
letter-spacing: 0.44px;
text-transform: uppercase;
&::before {
background-color: transparent;
}
}
.ant-table-thead > tr > th:has(.hostname-column-header) {
background: var(--bg-ink-400);
}
.ant-table-cell {
padding: 12px;
font-size: 13px;
line-height: 20px;
color: var(--bg-vanilla-100);
background: var(--bg-ink-500);
}
.ant-table-cell:has(.hostname-column-value) {
background: var(--bg-ink-400);
}
.hostname-column-value {
color: var(--Vanilla-100, #fff);
font-family: 'Geist Mono';
font-style: normal;
font-weight: 600;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.status-cell {
.active-tag {
color: var(--bg-forest-500);
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
}
.progress-container {
.ant-progress-bg {
height: 8px !important;
border-radius: 4px;
}
}
.ant-table-tbody > tr:hover > td {
background: rgba(255, 255, 255, 0.04);
}
.ant-table-cell:first-child {
text-align: justify;
}
.ant-table-cell:nth-child(2) {
padding-left: 16px;
padding-right: 16px;
}
.ant-table-cell:nth-child(n + 3) {
padding-right: 24px;
}
.column-header-right {
text-align: right;
}
.ant-table-tbody > tr > td {
border-bottom: none;
}
.ant-table-thead
> tr
> th:not(:last-child):not(.ant-table-selection-column):not(.ant-table-row-expand-icon-cell):not([colspan])::before {
background-color: transparent;
}
.ant-empty-normal {
visibility: hidden;
}
}
.ant-pagination {
position: fixed;
bottom: 0;
width: calc(100% - 64px);
background: var(--bg-ink-500);
padding: 16px;
margin: 0;
// this is to offset intercom icon till we improve the design
padding-right: 72px;
.ant-pagination-item {
border-radius: 4px;
&-active {
background: var(--bg-robin-500);
border-color: var(--bg-robin-500);
a {
color: var(--bg-ink-500) !important;
}
}
}
}
}
}
.infra-monitoring-tags {
width: fit-content;
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 18px; /* 163.636% */
letter-spacing: 0.44px;
text-transform: uppercase;
border-radius: 50px;
padding: 2px 8px;
&.active {
color: var(--Forest-500, #25e192);
border: 1px solid rgba(37, 225, 146, 0.2);
background: rgba(37, 225, 146, 0.1);
}
&.inactive {
color: var(--Slate-50, #62687c);
border: 1px solid rgba(98, 104, 124, 0.2);
background: rgba(98, 104, 124, 0.1);
}
}
.lightMode {
.infra-monitoring-container {
.ant-table-thead > tr > th {
background: var(--bg-vanilla-100);
color: var(--bg-ink-500);
}
.ant-table-cell {
color: var(--bg-ink-500);
}
.hosts-list-controls {
border-top: 1px solid var(--bg-vanilla-300);
border-bottom: 1px solid var(--bg-vanilla-300);
.ant-select-selector {
border-color: var(--bg-vanilla-300) !important;
background-color: var(--bg-vanilla-100) !important;
color: var(--bg-ink-200);
}
}
}
.hosts-list-table {
.ant-table {
.ant-table-thead > tr > th {
background: var(--bg-vanilla-100);
color: var(--text-ink-300);
}
.ant-table-thead > tr > th:has(.hostname-column-header) {
background: var(--bg-vanilla-100);
}
.ant-table-cell {
background: var(--bg-vanilla-100);
color: var(--bg-ink-500);
}
.ant-table-cell:has(.hostname-column-value) {
background: var(--bg-vanilla-100);
}
.hostname-column-value {
color: var(--bg-ink-300);
}
.ant-table-tbody > tr:hover > td {
background: rgba(0, 0, 0, 0.04);
}
}
.ant-pagination {
background: var(--bg-vanilla-100);
.ant-pagination-item {
&-active {
background: var(--bg-robin-500);
border-color: var(--bg-robin-500);
a {
color: var(--bg-vanilla-100) !important;
}
}
}
}
}
}

View File

@ -0,0 +1,75 @@
import './InfraMonitoring.styles.scss';
import * as Sentry from '@sentry/react';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { useEffect } from 'react';
import { DataSource } from 'types/common/queryBuilder';
import HostsList from './HostsList';
function InfraMonitoringHosts(): JSX.Element {
const {
updateAllQueriesOperators,
handleSetConfig,
setSupersetQuery,
setLastUsedQuery,
currentQuery,
resetQuery,
} = useQueryBuilder();
useEffect(() => {
const newQuery = updateAllQueriesOperators(
initialQueriesMap.metrics,
PANEL_TYPES.TIME_SERIES,
DataSource.METRICS,
);
setSupersetQuery(newQuery);
setLastUsedQuery(0);
handleSetConfig(PANEL_TYPES.TIME_SERIES, DataSource.METRICS);
return (): void => {
setLastUsedQuery(0);
};
}, [
updateAllQueriesOperators,
setSupersetQuery,
setLastUsedQuery,
handleSetConfig,
]);
useEffect(() => {
const updatedCurrentQuery = {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
{
...currentQuery.builder.queryData[0],
filters: {
items: [],
op: 'AND',
},
},
],
},
};
resetQuery(updatedCurrentQuery);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className="infra-monitoring-container">
<div className="hosts-list-container">
<HostsList />
</div>
</div>
</Sentry.ErrorBoundary>
);
}
export default InfraMonitoringHosts;

View File

@ -0,0 +1,133 @@
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 TabLabel from 'components/TabLabel';
import { PANEL_TYPES } from 'constants/queryBuilder';
import HostsList from './HostsList';
export interface HostRowData {
hostName: string;
cpu: React.ReactNode;
memory: React.ReactNode;
wait: string;
load15: number;
active: React.ReactNode;
}
export const getHostListsQuery = (): HostListPayload => ({
filters: {
items: [],
op: 'and',
},
groupBy: [],
orderBy: { columnName: 'cpu', order: 'desc' },
});
export const getTabsItems = (): TabsProps['items'] => [
{
label: <TabLabel label="List View" isDisabled={false} tooltipText="" />,
key: PANEL_TYPES.LIST,
children: <HostsList />,
},
];
export const getHostsListColumns = (): ColumnType<HostRowData>[] => [
{
title: <div className="hostname-column-header">Hostname</div>,
dataIndex: 'hostName',
key: 'hostName',
width: 250,
render: (value: string): React.ReactNode => (
<div className="hostname-column-value">{value}</div>
),
},
{
title: 'Status',
dataIndex: 'active',
key: 'active',
width: 100,
},
{
title: <div className="column-header-right">CPU Usage</div>,
dataIndex: 'cpu',
key: 'cpu',
width: 100,
sorter: true,
align: 'right',
},
{
title: <div className="column-header-right">Memory Usage</div>,
dataIndex: 'memory',
key: 'memory',
width: 100,
sorter: true,
align: 'right',
},
{
title: <div className="column-header-right">IOWait</div>,
dataIndex: 'wait',
key: 'wait',
width: 100,
sorter: true,
align: 'right',
},
{
title: <div className="column-header-right">Load Avg</div>,
dataIndex: 'load15',
key: 'load15',
width: 100,
sorter: true,
align: 'right',
},
];
export const formatDataForTable = (data: HostData[]): HostRowData[] =>
data.map((host, index) => ({
key: `${host.hostName}-${index}`,
hostName: host.hostName || '',
active: (
<Tag
bordered
className={`infra-monitoring-tags ${host.active ? 'active' : 'inactive'}`}
>
{host.active ? 'ACTIVE' : 'INACTIVE'}
</Tag>
),
cpu: (
<div className="progress-container">
<Progress
percent={Number((host.cpu * 100).toFixed(1))}
strokeLinecap="butt"
size="small"
strokeColor={((): string => {
const cpuPercent = Number((host.cpu * 100).toFixed(1));
if (cpuPercent >= 90) return Color.BG_SAKURA_500;
if (cpuPercent >= 60) return Color.BG_AMBER_500;
return Color.BG_FOREST_500;
})()}
className="progress-bar"
/>
</div>
),
memory: (
<div className="progress-container">
<Progress
percent={Number((host.memory * 100).toFixed(1))}
strokeLinecap="butt"
size="small"
strokeColor={((): string => {
const memoryPercent = Number((host.memory * 100).toFixed(1));
if (memoryPercent >= 90) return Color.BG_CHERRY_500;
if (memoryPercent >= 60) return Color.BG_AMBER_500;
return Color.BG_FOREST_500;
})()}
className="progress-bar"
/>
</div>
),
wait: `${Number((host.wait * 100).toFixed(1))}%`,
load15: host.load15,
}));

View File

@ -438,6 +438,7 @@ function DashboardsList(): JSX.Element {
const getLink = (): string => `${ROUTES.ALL_DASHBOARD}/${dashboard.id}`; const getLink = (): string => `${ROUTES.ALL_DASHBOARD}/${dashboard.id}`;
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => { const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
event.stopPropagation();
if (event.metaKey || event.ctrlKey) { if (event.metaKey || event.ctrlKey) {
window.open(getLink(), '_blank'); window.open(getLink(), '_blank');
} else { } else {
@ -458,7 +459,11 @@ function DashboardsList(): JSX.Element {
placement="left" placement="left"
overlayClassName="title-toolip" overlayClassName="title-toolip"
> >
<Link to={getLink()} className="title-link"> <Link
to={getLink()}
className="title-link"
onClick={(e): void => e.stopPropagation()}
>
<img <img
src={dashboard?.image || Base64Icons[0]} src={dashboard?.image || Base64Icons[0]}
alt="dashboard-image" alt="dashboard-image"

View File

@ -9,7 +9,7 @@ import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query'; import { QueryParams } from 'constants/query';
import Header from 'container/OnboardingContainer/common/Header/Header'; import Header from 'container/OnboardingContainer/common/Header/Header';
import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext'; import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext';
import { useOnboardingStatus } from 'hooks/messagingQueue / onboarding/useOnboardingStatus'; import { useOnboardingStatus } from 'hooks/messagingQueue/useOnboardingStatus';
import { useQueryService } from 'hooks/useQueryService'; import { useQueryService } from 'hooks/useQueryService';
import useResourceAttribute from 'hooks/useResourceAttribute'; import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils'; import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';

View File

@ -8,6 +8,7 @@ import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import { themeColors } from 'constants/theme'; import { themeColors } from 'constants/theme';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { generateColor } from 'lib/uPlotLib/utils/generateColor'; import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import { isNaN } from 'lodash-es';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { Query } from 'types/api/queryBuilder/queryBuilderData';
@ -44,7 +45,7 @@ function PiePanelWrapper({
const isDarkMode = useIsDarkMode(); const isDarkMode = useIsDarkMode();
const pieChartData: { let pieChartData: {
label: string; label: string;
value: string; value: string;
color: string; color: string;
@ -67,6 +68,10 @@ function PiePanelWrapper({
) )
.filter((d) => d !== undefined) as never[]), .filter((d) => d !== undefined) as never[]),
); );
pieChartData = pieChartData.filter(
(arc) =>
arc.value && !isNaN(parseFloat(arc.value)) && parseFloat(arc.value) > 0,
);
let size = 0; let size = 0;
let width = 0; let width = 0;
@ -108,7 +113,7 @@ function PiePanelWrapper({
if (!active) return half - 3; if (!active) return half - 3;
return data.label === active.label ? half : half - 3; return data.label === active.label ? half : half - 3;
}} }}
padAngle={0.02} padAngle={0.01}
cornerRadius={3} cornerRadius={3}
width={size} width={size}
height={size} height={size}

View File

@ -72,6 +72,8 @@ function QueryBuilderSearch({
className, className,
placeholder, placeholder,
suffixIcon, suffixIcon,
isInfraMonitoring,
disableNavigationShortcuts,
}: QueryBuilderSearchProps): JSX.Element { }: QueryBuilderSearchProps): JSX.Element {
const { pathname } = useLocation(); const { pathname } = useLocation();
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [ const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
@ -93,7 +95,12 @@ function QueryBuilderSearch({
searchKey, searchKey,
key, key,
exampleQueries, exampleQueries,
} = useAutoComplete(query, whereClauseConfig, isLogsExplorerPage); } = useAutoComplete(
query,
whereClauseConfig,
isLogsExplorerPage,
isInfraMonitoring,
);
const [isOpen, setIsOpen] = useState<boolean>(false); const [isOpen, setIsOpen] = useState<boolean>(false);
const [showAllFilters, setShowAllFilters] = useState<boolean>(false); const [showAllFilters, setShowAllFilters] = useState<boolean>(false);
const [dynamicPlacholder, setDynamicPlaceholder] = useState<string>( const [dynamicPlacholder, setDynamicPlaceholder] = useState<string>(
@ -105,6 +112,7 @@ function QueryBuilderSearch({
query, query,
searchKey, searchKey,
isLogsExplorerPage, isLogsExplorerPage,
isInfraMonitoring,
); );
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys(); const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
@ -162,14 +170,22 @@ function QueryBuilderSearch({
if (isMulti || event.key === 'Backspace') handleKeyDown(event); if (isMulti || event.key === 'Backspace') handleKeyDown(event);
if (isExistsNotExistsOperator(searchValue)) handleKeyDown(event); if (isExistsNotExistsOperator(searchValue)) handleKeyDown(event);
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') { if (
!disableNavigationShortcuts &&
(event.ctrlKey || event.metaKey) &&
event.key === 'Enter'
) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
handleRunQuery(); handleRunQuery();
setIsOpen(false); setIsOpen(false);
} }
if ((event.ctrlKey || event.metaKey) && event.key === '/') { if (
!disableNavigationShortcuts &&
(event.ctrlKey || event.metaKey) &&
event.key === '/'
) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
setShowAllFilters((prev) => !prev); setShowAllFilters((prev) => !prev);
@ -185,8 +201,8 @@ function QueryBuilderSearch({
); );
const isMetricsDataSource = useMemo( const isMetricsDataSource = useMemo(
() => query.dataSource === DataSource.METRICS, () => query.dataSource === DataSource.METRICS && !isInfraMonitoring,
[query.dataSource], [query.dataSource, isInfraMonitoring],
); );
const fetchValueDataType = (value: unknown, operator: string): DataTypes => { const fetchValueDataType = (value: unknown, operator: string): DataTypes => {
@ -250,7 +266,7 @@ function QueryBuilderSearch({
); );
useEffect(() => { useEffect(() => {
if (isLastQuery) { if (isLastQuery && !disableNavigationShortcuts) {
registerShortcut(LogsExplorerShortcuts.FocusTheSearchBar, () => { registerShortcut(LogsExplorerShortcuts.FocusTheSearchBar, () => {
// set timeout is needed here else the select treats the hotkey as input value // set timeout is needed here else the select treats the hotkey as input value
setTimeout(() => { setTimeout(() => {
@ -261,7 +277,12 @@ function QueryBuilderSearch({
return (): void => return (): void =>
deregisterShortcut(LogsExplorerShortcuts.FocusTheSearchBar); deregisterShortcut(LogsExplorerShortcuts.FocusTheSearchBar);
}, [deregisterShortcut, isLastQuery, registerShortcut]); }, [
deregisterShortcut,
disableNavigationShortcuts,
isLastQuery,
registerShortcut,
]);
useEffect(() => { useEffect(() => {
if (!isOpen) { if (!isOpen) {
@ -427,6 +448,8 @@ interface QueryBuilderSearchProps {
className?: string; className?: string;
placeholder?: string; placeholder?: string;
suffixIcon?: React.ReactNode; suffixIcon?: React.ReactNode;
isInfraMonitoring?: boolean;
disableNavigationShortcuts?: boolean;
} }
QueryBuilderSearch.defaultProps = { QueryBuilderSearch.defaultProps = {
@ -434,6 +457,8 @@ QueryBuilderSearch.defaultProps = {
className: '', className: '',
placeholder: PLACEHOLDER, placeholder: PLACEHOLDER,
suffixIcon: undefined, suffixIcon: undefined,
isInfraMonitoring: false,
disableNavigationShortcuts: false,
}; };
export interface CustomTagProps { export interface CustomTagProps {

View File

@ -119,6 +119,22 @@ function SideNav({
setMenuItems(items); setMenuItems(items);
} }
const isInfraMonitoringEnabled =
featureResponse.data?.find(
(feature) => feature.name === FeatureKeys.HOSTS_INFRA_MONITORING,
)?.active || false;
if (!isInfraMonitoringEnabled) {
let items = [...menuItems];
items = items.filter(
(item) => item.key !== ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
);
setMenuItems(items);
}
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [featureResponse.data]); }, [featureResponse.data]);

View File

@ -11,6 +11,7 @@ import {
LayoutGrid, LayoutGrid,
ListMinus, ListMinus,
MessageSquare, MessageSquare,
PackagePlus,
Receipt, Receipt,
Route, Route,
ScrollText, ScrollText,
@ -118,6 +119,12 @@ const menuItems: SidebarItem[] = [
label: 'Billing', label: 'Billing',
icon: <Receipt size={16} />, icon: <Receipt size={16} />,
}, },
{
key: ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
label: 'Infra Monitoring',
icon: <PackagePlus size={16} />,
isBeta: true,
},
{ {
key: ROUTES.SETTINGS, key: ROUTES.SETTINGS,
label: 'Settings', label: 'Settings',

View File

@ -212,6 +212,7 @@ export const routesToSkip = [
ROUTES.ALERT_OVERVIEW, ROUTES.ALERT_OVERVIEW,
ROUTES.MESSAGING_QUEUES, ROUTES.MESSAGING_QUEUES,
ROUTES.MESSAGING_QUEUES_DETAIL, ROUTES.MESSAGING_QUEUES_DETAIL,
ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
]; ];
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS]; export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];

View File

@ -68,6 +68,9 @@ function DateTimeSelection({
showResetButton = false, showResetButton = false,
showOldExplorerCTA = false, showOldExplorerCTA = false,
defaultRelativeTime = RelativeTimeMap['6hr'] as Time, defaultRelativeTime = RelativeTimeMap['6hr'] as Time,
isModalTimeSelection = false,
onTimeChange,
modalSelectedInterval,
}: Props): JSX.Element { }: Props): JSX.Element {
const [formSelector] = Form.useForm(); const [formSelector] = Form.useForm();
@ -205,7 +208,6 @@ function DateTimeSelection({
return `${startString} - ${endString}`; return `${startString} - ${endString}`;
} }
return timeInterval; return timeInterval;
}; };
@ -219,6 +221,12 @@ function DateTimeSelection({
} }
}, [selectedTime]); }, [selectedTime]);
useEffect(() => {
if (isModalTimeSelection && modalSelectedInterval === 'custom') {
setCustomDTPickerVisible(true);
}
}, [isModalTimeSelection, modalSelectedInterval]);
const getDefaultTime = (pathName: string): Time => { const getDefaultTime = (pathName: string): Time => {
const defaultSelectedOption = getDefaultOption(pathName); const defaultSelectedOption = getDefaultOption(pathName);
@ -306,6 +314,15 @@ function DateTimeSelection({
const onSelectHandler = useCallback( const onSelectHandler = useCallback(
(value: Time | CustomTimeType): void => { (value: Time | CustomTimeType): void => {
if (isModalTimeSelection) {
if (value === 'custom') {
setCustomDTPickerVisible(true);
setIsValidteRelativeTime(false);
return;
}
onTimeChange?.(value);
return;
}
if (value !== 'custom') { if (value !== 'custom') {
setIsOpen(false); setIsOpen(false);
updateTimeInterval(value); updateTimeInterval(value);
@ -345,7 +362,9 @@ function DateTimeSelection({
[ [
initQueryBuilderData, initQueryBuilderData,
isLogsExplorerPage, isLogsExplorerPage,
isModalTimeSelection,
location.pathname, location.pathname,
onTimeChange,
refreshButtonHidden, refreshButtonHidden,
stagedQuery, stagedQuery,
updateLocalStorageForRoutes, updateLocalStorageForRoutes,
@ -364,9 +383,34 @@ function DateTimeSelection({
} }
}, [defaultRelativeTime, onSelectHandler]); }, [defaultRelativeTime, onSelectHandler]);
const [modalStartTime, setModalStartTime] = useState<number>(0);
const [modalEndTime, setModalEndTime] = useState<number>(0);
// eslint-disable-next-line sonarjs/cognitive-complexity
const onCustomDateHandler = (dateTimeRange: DateTimeRangeType): void => { const onCustomDateHandler = (dateTimeRange: DateTimeRangeType): void => {
if (dateTimeRange !== null) { if (dateTimeRange !== null) {
const [startTimeMoment, endTimeMoment] = dateTimeRange; const [startTimeMoment, endTimeMoment] = dateTimeRange;
if (isModalTimeSelection) {
if (!startTimeMoment || !endTimeMoment) {
setHasSelectedTimeError(true);
return;
}
const startTs = startTimeMoment.toDate().getTime();
const endTs = endTimeMoment.toDate().getTime();
if (startTs >= endTs) {
setHasSelectedTimeError(true);
return;
}
setCustomDTPickerVisible(false);
setHasSelectedTimeError(false);
setModalStartTime(startTs);
setModalEndTime(endTs);
onTimeChange?.('custom', [startTs, endTs]);
return;
}
if (startTimeMoment && endTimeMoment) { if (startTimeMoment && endTimeMoment) {
const startTime = startTimeMoment; const startTime = startTimeMoment;
const endTime = endTimeMoment; const endTime = endTimeMoment;
@ -397,6 +441,10 @@ function DateTimeSelection({
}; };
const onValidCustomDateHandler = (dateTimeStr: CustomTimeType): void => { const onValidCustomDateHandler = (dateTimeStr: CustomTimeType): void => {
if (isModalTimeSelection) {
onTimeChange?.(dateTimeStr);
return;
}
setIsOpen(false); setIsOpen(false);
updateTimeInterval(dateTimeStr); updateTimeInterval(dateTimeStr);
updateLocalStorageForRoutes(dateTimeStr); updateLocalStorageForRoutes(dateTimeStr);
@ -452,7 +500,6 @@ function DateTimeSelection({
if (OLD_RELATIVE_TIME_VALUES.indexOf(time) > -1) { if (OLD_RELATIVE_TIME_VALUES.indexOf(time) > -1) {
return convertOldTimeToNewValidCustomTimeFormat(time); return convertOldTimeToNewValidCustomTimeFormat(time);
} }
return time; return time;
}; };
@ -656,7 +703,9 @@ function DateTimeSelection({
onError={(hasError: boolean): void => { onError={(hasError: boolean): void => {
setHasSelectedTimeError(hasError); setHasSelectedTimeError(hasError);
}} }}
selectedTime={selectedTime} selectedTime={
isModalTimeSelection ? (modalSelectedInterval as Time) : selectedTime
}
onValidCustomDateChange={(dateTime): void => { onValidCustomDateChange={(dateTime): void => {
onValidCustomDateHandler(dateTime.timeStr as CustomTimeType); onValidCustomDateHandler(dateTime.timeStr as CustomTimeType);
}} }}
@ -664,9 +713,9 @@ function DateTimeSelection({
setIsValidteRelativeTime(isValid); setIsValidteRelativeTime(isValid);
}} }}
selectedValue={getInputLabel( selectedValue={getInputLabel(
dayjs(minTime / 1000000), dayjs(isModalTimeSelection ? modalStartTime : minTime / 1000000),
dayjs(maxTime / 1000000), dayjs(isModalTimeSelection ? modalEndTime : maxTime / 1000000),
selectedTime, isModalTimeSelection ? modalSelectedInterval : selectedTime,
)} )}
data-testid="dropDown" data-testid="dropDown"
items={options} items={options}
@ -722,6 +771,12 @@ interface DateTimeSelectionV2Props {
showOldExplorerCTA?: boolean; showOldExplorerCTA?: boolean;
showResetButton?: boolean; showResetButton?: boolean;
defaultRelativeTime?: Time; defaultRelativeTime?: Time;
isModalTimeSelection?: boolean;
onTimeChange?: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
modalSelectedInterval?: Time;
} }
DateTimeSelection.defaultProps = { DateTimeSelection.defaultProps = {
@ -730,6 +785,9 @@ DateTimeSelection.defaultProps = {
showRefreshText: true, showRefreshText: true,
showResetButton: false, showResetButton: false,
defaultRelativeTime: RelativeTimeMap['6hr'] as Time, defaultRelativeTime: RelativeTimeMap['6hr'] as Time,
isModalTimeSelection: false,
onTimeChange: (): void => {},
modalSelectedInterval: RelativeTimeMap['5m'] as Time,
}; };
interface DispatchProps { interface DispatchProps {
updateTimeInterval: ( updateTimeInterval: (
@ -741,8 +799,31 @@ interface DispatchProps {
const mapDispatchToProps = ( const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>, dispatch: ThunkDispatch<unknown, unknown, AppActions>,
{ isModalTimeSelection }: DateTimeSelectionV2Props,
): DispatchProps => ({ ): DispatchProps => ({
updateTimeInterval: bindActionCreators(UpdateTimeInterval, dispatch), updateTimeInterval: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
): ((dispatch: Dispatch<AppActions>) => void) => {
/**
* Updates the global time interval only when not in modal view
*
* @param interval - Selected time interval or custom time range
* @param dateTimeRange - Optional tuple of [startTime, endTime]
* @returns Function that updates redux store with new time interval, or empty function for modal view
*
* When in modal view (isModalTimeSelection=true), we don't want to update the global time state
* as the selection is temporary until the modal is confirmed
*/
if (!isModalTimeSelection) {
return bindActionCreators(UpdateTimeInterval, dispatch)(
interval,
dateTimeRange,
);
}
// Return empty function for modal view as we don't want to update global state
return (): void => {};
},
globalTimeLoading: bindActionCreators(GlobalTimeLoading, dispatch), globalTimeLoading: bindActionCreators(GlobalTimeLoading, dispatch),
}); });

View File

@ -12,6 +12,7 @@ function TraceExplorerControls({
totalCount, totalCount,
perPageOptions, perPageOptions,
config, config,
showSizeChanger = true,
}: TraceExplorerControlsProps): JSX.Element | null { }: TraceExplorerControlsProps): JSX.Element | null {
const { const {
pagination, pagination,
@ -38,6 +39,7 @@ function TraceExplorerControls({
handleCountItemsPerPageChange={handleCountItemsPerPageChange} handleCountItemsPerPageChange={handleCountItemsPerPageChange}
handleNavigateNext={handleNavigateNext} handleNavigateNext={handleNavigateNext}
handleNavigatePrevious={handleNavigatePrevious} handleNavigatePrevious={handleNavigatePrevious}
showSizeChanger={showSizeChanger}
/> />
</Container> </Container>
); );
@ -52,6 +54,11 @@ type TraceExplorerControlsProps = Pick<
'isLoading' | 'totalCount' | 'perPageOptions' 'isLoading' | 'totalCount' | 'perPageOptions'
> & { > & {
config?: OptionsMenuConfig | null; config?: OptionsMenuConfig | null;
showSizeChanger?: boolean;
};
TraceExplorerControls.defaultProps = {
showSizeChanger: true,
}; };
export default memo(TraceExplorerControls); export default memo(TraceExplorerControls);

View File

@ -10,16 +10,22 @@ import { ILog } from 'types/api/logs/log';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { QueryDataV3 } from 'types/api/widgets/getQuery'; import { QueryDataV3 } from 'types/api/widgets/getQuery';
function BlockLink({ export function BlockLink({
children, children,
to, to,
openInNewTab,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
to: string; to: string;
openInNewTab: boolean;
}): any { }): any {
// Display block to make the whole cell clickable // Display block to make the whole cell clickable
return ( return (
<Link to={to} style={{ display: 'block' }}> <Link
to={to}
style={{ display: 'block' }}
target={openInNewTab ? '_blank' : '_self'}
>
{children} {children}
</Link> </Link>
); );
@ -53,7 +59,7 @@ export const getListColumns = (
? dayjs(value).format('YYYY-MM-DD HH:mm:ss.SSS') ? dayjs(value).format('YYYY-MM-DD HH:mm:ss.SSS')
: dayjs(value / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS'); : dayjs(value / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS');
return ( return (
<BlockLink to={getTraceLink(item)}> <BlockLink to={getTraceLink(item)} openInNewTab={false}>
<Typography.Text>{date}</Typography.Text> <Typography.Text>{date}</Typography.Text>
</BlockLink> </BlockLink>
); );
@ -70,7 +76,7 @@ export const getListColumns = (
render: (value, item): JSX.Element => { render: (value, item): JSX.Element => {
if (value === '') { if (value === '') {
return ( return (
<BlockLink to={getTraceLink(item)}> <BlockLink to={getTraceLink(item)} openInNewTab={false}>
<Typography data-testid={key}>N/A</Typography> <Typography data-testid={key}>N/A</Typography>
</BlockLink> </BlockLink>
); );
@ -78,7 +84,7 @@ export const getListColumns = (
if (key === 'httpMethod' || key === 'responseStatusCode') { if (key === 'httpMethod' || key === 'responseStatusCode') {
return ( return (
<BlockLink to={getTraceLink(item)}> <BlockLink to={getTraceLink(item)} openInNewTab={false}>
<Tag data-testid={key} color="magenta"> <Tag data-testid={key} color="magenta">
{value} {value}
</Tag> </Tag>
@ -88,14 +94,14 @@ export const getListColumns = (
if (key === 'durationNano') { if (key === 'durationNano') {
return ( return (
<BlockLink to={getTraceLink(item)}> <BlockLink to={getTraceLink(item)} openInNewTab={false}>
<Typography data-testid={key}>{getMs(value)}ms</Typography> <Typography data-testid={key}>{getMs(value)}ms</Typography>
</BlockLink> </BlockLink>
); );
} }
return ( return (
<BlockLink to={getTraceLink(item)}> <BlockLink to={getTraceLink(item)} openInNewTab={false}>
<Typography data-testid={key}>{value}</Typography> <Typography data-testid={key}>{value}</Typography>
</BlockLink> </BlockLink>
); );

View File

@ -0,0 +1,34 @@
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
import { QueryBuilderKeys } from 'constants/queryBuilder';
import { useMemo } from 'react';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { IGetAttributeKeysPayload } from 'types/api/queryBuilder/getAttributeKeys';
import { IQueryAutocompleteResponse } from 'types/api/queryBuilder/queryAutocompleteResponse';
type UseGetAttributeKeys = (
requestData: IGetAttributeKeysPayload,
options?: UseQueryOptions<
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
>,
) => UseQueryResult<
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
>;
export const useGetAggregateKeys: UseGetAttributeKeys = (
requestData,
options,
) => {
const queryKey = useMemo(() => {
if (options?.queryKey && Array.isArray(options.queryKey)) {
return [QueryBuilderKeys.GET_AGGREGATE_KEYS, ...options.queryKey];
}
return [QueryBuilderKeys.GET_AGGREGATE_KEYS, requestData];
}, [options?.queryKey, requestData]);
return useQuery<SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse>({
queryKey,
queryFn: () => getAggregateKeys(requestData),
...options,
});
};

View File

@ -0,0 +1,42 @@
import {
getHostLists,
HostListPayload,
HostListResponse,
} from 'api/infraMonitoring/getHostLists';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useMemo } from 'react';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
type UseGetHostList = (
requestData: HostListPayload,
options?: UseQueryOptions<
SuccessResponse<HostListResponse> | ErrorResponse,
Error
>,
headers?: Record<string, string>,
) => UseQueryResult<SuccessResponse<HostListResponse> | ErrorResponse, Error>;
export const useGetHostList: UseGetHostList = (
requestData,
options,
headers,
) => {
const queryKey = useMemo(() => {
if (options?.queryKey && Array.isArray(options.queryKey)) {
return [...options.queryKey];
}
if (options?.queryKey && typeof options.queryKey === 'string') {
return options.queryKey;
}
return [REACT_QUERY_KEY.GET_HOST_LIST, requestData];
}, [options?.queryKey, requestData]);
return useQuery<SuccessResponse<HostListResponse> | ErrorResponse, Error>({
queryFn: ({ signal }) => getHostLists(requestData, signal, headers),
...options,
queryKey,
});
};

View File

@ -28,6 +28,7 @@ export const useAutoComplete = (
query: IBuilderQuery, query: IBuilderQuery,
whereClauseConfig?: WhereClauseConfig, whereClauseConfig?: WhereClauseConfig,
shouldUseSuggestions?: boolean, shouldUseSuggestions?: boolean,
isInfraMonitoring?: boolean,
): IAutoComplete => { ): IAutoComplete => {
const [searchValue, setSearchValue] = useState<string>(''); const [searchValue, setSearchValue] = useState<string>('');
const [searchKey, setSearchKey] = useState<string>(''); const [searchKey, setSearchKey] = useState<string>('');
@ -37,6 +38,7 @@ export const useAutoComplete = (
query, query,
searchKey, searchKey,
shouldUseSuggestions, shouldUseSuggestions,
isInfraMonitoring,
); );
const [key, operator, result] = useSetCurrentKeyAndOperator(searchValue, keys); const [key, operator, result] = useSetCurrentKeyAndOperator(searchValue, keys);
@ -170,4 +172,5 @@ interface IAutoComplete {
searchKey: string; searchKey: string;
key: string; key: string;
exampleQueries: TagFilter[]; exampleQueries: TagFilter[];
isInfraMonitoring?: boolean;
} }

View File

@ -1,3 +1,4 @@
import { getInfraAttributesValues } from 'api/infraMonitoring/getInfraAttributeValues';
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues'; import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig'; import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import { import {
@ -43,6 +44,7 @@ export const useFetchKeysAndValues = (
query: IBuilderQuery, query: IBuilderQuery,
searchKey: string, searchKey: string,
shouldUseSuggestions?: boolean, shouldUseSuggestions?: boolean,
isInfraMonitoring?: boolean,
): IuseFetchKeysAndValues => { ): IuseFetchKeysAndValues => {
const [keys, setKeys] = useState<BaseAutocompleteData[]>([]); const [keys, setKeys] = useState<BaseAutocompleteData[]>([]);
const [exampleQueries, setExampleQueries] = useState<TagFilter[]>([]); const [exampleQueries, setExampleQueries] = useState<TagFilter[]>([]);
@ -91,10 +93,10 @@ export const useFetchKeysAndValues = (
const isQueryEnabled = useMemo( const isQueryEnabled = useMemo(
() => () =>
query.dataSource === DataSource.METRICS query.dataSource === DataSource.METRICS && !isInfraMonitoring
? !!query.dataSource && !!query.aggregateAttribute.dataType ? !!query.dataSource && !!query.aggregateAttribute.dataType
: true, : true,
[query.aggregateAttribute.dataType, query.dataSource], [isInfraMonitoring, query.aggregateAttribute.dataType, query.dataSource],
); );
const { data, isFetching, status } = useGetAggregateKeys( const { data, isFetching, status } = useGetAggregateKeys(
@ -109,6 +111,7 @@ export const useFetchKeysAndValues = (
queryKey: [searchParams], queryKey: [searchParams],
enabled: isQueryEnabled && !shouldUseSuggestions, enabled: isQueryEnabled && !shouldUseSuggestions,
}, },
isInfraMonitoring,
); );
const { const {
@ -136,6 +139,7 @@ export const useFetchKeysAndValues = (
value: string, value: string,
query: IBuilderQuery, query: IBuilderQuery,
keys: BaseAutocompleteData[], keys: BaseAutocompleteData[],
// eslint-disable-next-line sonarjs/cognitive-complexity
): Promise<void> => { ): Promise<void> => {
if (!value) { if (!value) {
return; return;
@ -152,17 +156,36 @@ export const useFetchKeysAndValues = (
setAggregateFetching(true); setAggregateFetching(true);
try { try {
const { payload } = await getAttributesValues({ let payload;
if (isInfraMonitoring) {
const response = await getInfraAttributesValues({
dataSource: DataSource.METRICS,
attributeKey: filterAttributeKey?.key ?? tagKey,
filterAttributeKeyDataType:
filterAttributeKey?.dataType ?? DataTypes.EMPTY,
tagType: filterAttributeKey?.type ?? '',
searchText: isInNInOperator(tagOperator)
? tagValue[tagValue.length - 1]?.toString() ?? ''
: tagValue?.toString() ?? '',
aggregateOperator: query.aggregateOperator,
aggregateAttribute: query.aggregateAttribute.key,
});
payload = response.payload;
} else {
const response = await getAttributesValues({
aggregateOperator: query.aggregateOperator, aggregateOperator: query.aggregateOperator,
dataSource: query.dataSource, dataSource: query.dataSource,
aggregateAttribute: query.aggregateAttribute.key, aggregateAttribute: query.aggregateAttribute.key,
attributeKey: filterAttributeKey?.key ?? tagKey, attributeKey: filterAttributeKey?.key ?? tagKey,
filterAttributeKeyDataType: filterAttributeKey?.dataType ?? DataTypes.EMPTY, filterAttributeKeyDataType:
filterAttributeKey?.dataType ?? DataTypes.EMPTY,
tagType: filterAttributeKey?.type ?? '', tagType: filterAttributeKey?.type ?? '',
searchText: isInNInOperator(tagOperator) searchText: isInNInOperator(tagOperator)
? tagValue[tagValue.length - 1]?.toString() ?? '' // last element of tagvalue will be always user search value ? tagValue[tagValue.length - 1]?.toString() ?? ''
: tagValue?.toString() ?? '', : tagValue?.toString() ?? '',
}); });
payload = response.payload;
}
if (payload) { if (payload) {
const values = Object.values(payload).find((el) => !!el) || []; const values = Object.values(payload).find((el) => !!el) || [];

View File

@ -1,3 +1,4 @@
import { getHostAttributeKeys } from 'api/infra/getHostAttributeKeys';
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys'; import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
import { QueryBuilderKeys } from 'constants/queryBuilder'; import { QueryBuilderKeys } from 'constants/queryBuilder';
import { useMemo } from 'react'; import { useMemo } from 'react';
@ -11,6 +12,7 @@ type UseGetAttributeKeys = (
options?: UseQueryOptions< options?: UseQueryOptions<
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
>, >,
isInfraMonitoring?: boolean,
) => UseQueryResult< ) => UseQueryResult<
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
>; >;
@ -18,17 +20,25 @@ type UseGetAttributeKeys = (
export const useGetAggregateKeys: UseGetAttributeKeys = ( export const useGetAggregateKeys: UseGetAttributeKeys = (
requestData, requestData,
options, options,
isInfraMonitoring,
) => { ) => {
const queryKey = useMemo(() => { const queryKey = useMemo(() => {
if (options?.queryKey && Array.isArray(options.queryKey)) { if (options?.queryKey && Array.isArray(options.queryKey)) {
return [QueryBuilderKeys.GET_AGGREGATE_KEYS, ...options.queryKey]; return [
QueryBuilderKeys.GET_AGGREGATE_KEYS,
...options.queryKey,
isInfraMonitoring,
];
} }
return [QueryBuilderKeys.GET_AGGREGATE_KEYS, requestData]; return [QueryBuilderKeys.GET_AGGREGATE_KEYS, requestData, isInfraMonitoring];
}, [options?.queryKey, requestData]); }, [options?.queryKey, requestData, isInfraMonitoring]);
return useQuery<SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse>({ return useQuery<SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse>({
queryKey, queryKey,
queryFn: () => getAggregateKeys(requestData), queryFn: () =>
isInfraMonitoring
? getHostAttributeKeys(requestData.searchText)
: getAggregateKeys(requestData),
...options, ...options,
}); });
}; };

View File

@ -12,7 +12,7 @@ import {
} from 'container/TopNav/DateTimeSelectionV2/config'; } from 'container/TopNav/DateTimeSelectionV2/config';
import { Pagination } from 'hooks/queryPagination'; import { Pagination } from 'hooks/queryPagination';
import { convertNewDataToOld } from 'lib/newQueryBuilder/convertNewDataToOld'; import { convertNewDataToOld } from 'lib/newQueryBuilder/convertNewDataToOld';
import { isEmpty, cloneDeep } from 'lodash-es'; import { isEmpty } from 'lodash-es';
import { SuccessResponse } from 'types/api'; import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { Query } from 'types/api/queryBuilder/queryBuilderData';
@ -24,6 +24,7 @@ export async function GetMetricQueryRange(
version: string, version: string,
signal?: AbortSignal, signal?: AbortSignal,
headers?: Record<string, string>, headers?: Record<string, string>,
isInfraMonitoring?: boolean,
): Promise<SuccessResponse<MetricRangePayloadProps>> { ): Promise<SuccessResponse<MetricRangePayloadProps>> {
const { legendMap, queryPayload } = prepareQueryRangePayload(props); const { legendMap, queryPayload } = prepareQueryRangePayload(props);
const response = await getMetricsQueryRange( const response = await getMetricsQueryRange(

View File

@ -0,0 +1,47 @@
.infra-monitoring-module-container {
flex: 1;
display: flex;
flex-direction: column;
.ant-tabs-nav {
padding: 0 8px;
margin-bottom: 0px;
&::before {
border-bottom: 1px solid var(--bg-slate-400) !important;
}
}
.ant-tabs-content-holder {
display: flex;
.ant-tabs-content {
flex: 1;
display: flex;
flex-direction: column;
.ant-tabs-tabpane {
flex: 1;
display: flex;
flex-direction: column;
}
}
}
.tab-item {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
}
}
.lightMode {
.infra-monitoring-module-container {
.ant-tabs-nav {
&::before {
border-bottom: 1px solid var(--bg-vanilla-300) !important;
}
}
}
}

View File

@ -0,0 +1,20 @@
import './InfrastructureMonitoring.styles.scss';
import RouteTab from 'components/RouteTab';
import { TabRoutes } from 'components/RouteTab/types';
import history from 'lib/history';
import { useLocation } from 'react-use';
import { Hosts } from './constants';
export default function InfrastructureMonitoringPage(): JSX.Element {
const { pathname } = useLocation();
const routes: TabRoutes[] = [Hosts];
return (
<div className="infra-monitoring-module-container">
<RouteTab routes={routes} activeKey={pathname} history={history} />
</div>
);
}

View File

@ -0,0 +1,15 @@
import { TabRoutes } from 'components/RouteTab/types';
import ROUTES from 'constants/routes';
import InfraMonitoringHosts from 'container/InfraMonitoringHosts';
import { Inbox } from 'lucide-react';
export const Hosts: TabRoutes = {
Component: InfraMonitoringHosts,
name: (
<div className="tab-item">
<Inbox size={16} /> Hosts
</div>
),
route: ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
key: ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
};

View File

@ -0,0 +1,3 @@
import InfrastructureMonitoringPage from './InfrastructureMonitoringPage';
export default InfrastructureMonitoringPage;

View File

@ -152,11 +152,7 @@ describe('Logs Explorer Tests', () => {
// check for loading state to be not present // check for loading state to be not present
await waitFor(() => await waitFor(() =>
expect( expect(queryByText(`Retrieving your logs!`)).not.toBeInTheDocument(),
queryByText(
`Just a bit of patience, just a little bits enough ⎯ were getting your logs!`,
),
).not.toBeInTheDocument(),
); );
// check for no data state to not be present // check for no data state to not be present

View File

@ -3,7 +3,7 @@ import './MessagingQueueHealthCheck.styles.scss';
import { Button } from 'antd'; import { Button } from 'antd';
import cx from 'classnames'; import cx from 'classnames';
import { useOnboardingStatus } from 'hooks/messagingQueue / onboarding/useOnboardingStatus'; import { useOnboardingStatus } from 'hooks/messagingQueue/useOnboardingStatus';
import { Bolt, FolderTree } from 'lucide-react'; import { Bolt, FolderTree } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';

View File

@ -104,4 +104,5 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
SHORTCUTS: ['ADMIN', 'EDITOR', 'VIEWER'], SHORTCUTS: ['ADMIN', 'EDITOR', 'VIEWER'],
INTEGRATIONS: ['ADMIN', 'EDITOR', 'VIEWER'], INTEGRATIONS: ['ADMIN', 'EDITOR', 'VIEWER'],
SERVICE_TOP_LEVEL_OPERATIONS: ['ADMIN', 'EDITOR', 'VIEWER'], SERVICE_TOP_LEVEL_OPERATIONS: ['ADMIN', 'EDITOR', 'VIEWER'],
INFRASTRUCTURE_MONITORING_HOSTS: ['ADMIN', 'EDITOR', 'VIEWER'],
}; };

View File

@ -4081,13 +4081,6 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/http-proxy@^1.17.8":
version "1.17.11"
resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.11.tgz"
integrity sha512-HC8G7c1WmaF2ekqpnFq626xd3Zz0uvaqFmBJNRZCGEZCXkvSdJoNFn/8Ygbd9fKNQj8UzLdCETaI0UWPAjK7IA==
dependencies:
"@types/node" "*"
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.4" version "2.0.4"
resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz" resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz"
@ -9359,7 +9352,7 @@ http-proxy-agent@^4.0.1:
agent-base "6" agent-base "6"
debug "4" debug "4"
http-proxy-middleware@3.0.3: http-proxy-middleware@3.0.3, http-proxy-middleware@^2.0.3:
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-3.0.3.tgz#dc1313c75bd00d81e103823802551ee30130ebd1" resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-3.0.3.tgz#dc1313c75bd00d81e103823802551ee30130ebd1"
integrity sha512-usY0HG5nyDUwtqpiZdETNbmKtw3QQ1jwYFZ9wi5iHzX2BcILwQKtYDJPo7XHTsu5Z0B2Hj3W9NNnbd+AjFWjqg== integrity sha512-usY0HG5nyDUwtqpiZdETNbmKtw3QQ1jwYFZ9wi5iHzX2BcILwQKtYDJPo7XHTsu5Z0B2Hj3W9NNnbd+AjFWjqg==
@ -9371,17 +9364,6 @@ http-proxy-middleware@3.0.3:
is-plain-object "^5.0.0" is-plain-object "^5.0.0"
micromatch "^4.0.8" micromatch "^4.0.8"
http-proxy-middleware@^2.0.3:
version "2.0.6"
resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f"
integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==
dependencies:
"@types/http-proxy" "^1.17.8"
http-proxy "^1.18.1"
is-glob "^4.0.1"
is-plain-obj "^3.0.0"
micromatch "^4.0.2"
http-proxy@^1.18.1: http-proxy@^1.18.1:
version "1.18.1" version "1.18.1"
resolved "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz" resolved "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz"
@ -9844,11 +9826,6 @@ is-plain-obj@^1.1.0:
resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz" resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz"
integrity sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg== integrity sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==
is-plain-obj@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz"
integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==
is-plain-obj@^4.0.0: is-plain-obj@^4.0.0:
version "4.1.0" version "4.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0"
@ -11851,7 +11828,7 @@ micromark@^3.0.0:
micromark-util-types "^1.0.1" micromark-util-types "^1.0.1"
uvu "^0.5.0" uvu "^0.5.0"
micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5, micromatch@^4.0.8: micromatch@^4.0.4, micromatch@^4.0.5, micromatch@^4.0.8:
version "4.0.8" version "4.0.8"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
@ -16549,7 +16526,7 @@ webpack-cli@^4.9.2:
rechoir "^0.7.0" rechoir "^0.7.0"
webpack-merge "^5.7.3" webpack-merge "^5.7.3"
webpack-dev-middleware@^5.3.1: webpack-dev-middleware@^5.3.1, webpack-dev-middleware@^5.3.4:
version "5.3.4" version "5.3.4"
resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz#eb7b39281cbce10e104eb2b8bf2b63fce49a3517" resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz#eb7b39281cbce10e104eb2b8bf2b63fce49a3517"
integrity sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q== integrity sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==
@ -16596,10 +16573,10 @@ webpack-dev-server@*:
webpack-dev-middleware "^5.3.1" webpack-dev-middleware "^5.3.1"
ws "^8.13.0" ws "^8.13.0"
webpack-dev-server@^4.15.1: webpack-dev-server@^4.15.2:
version "4.15.1" version "4.15.2"
resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz#8944b29c12760b3a45bdaa70799b17cb91b03df7" resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz#9e0c70a42a012560860adb186986da1248333173"
integrity sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA== integrity sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==
dependencies: dependencies:
"@types/bonjour" "^3.5.9" "@types/bonjour" "^3.5.9"
"@types/connect-history-api-fallback" "^1.3.5" "@types/connect-history-api-fallback" "^1.3.5"
@ -16629,7 +16606,7 @@ webpack-dev-server@^4.15.1:
serve-index "^1.9.1" serve-index "^1.9.1"
sockjs "^0.3.24" sockjs "^0.3.24"
spdy "^4.0.2" spdy "^4.0.2"
webpack-dev-middleware "^5.3.1" webpack-dev-middleware "^5.3.4"
ws "^8.13.0" ws "^8.13.0"
webpack-log@^3.0.1: webpack-log@^3.0.1:

21
go.mod
View File

@ -8,7 +8,7 @@ require (
github.com/ClickHouse/clickhouse-go/v2 v2.25.0 github.com/ClickHouse/clickhouse-go/v2 v2.25.0
github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd
github.com/SigNoz/signoz-otel-collector v0.111.9 github.com/SigNoz/signoz-otel-collector v0.111.13
github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974 github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974
github.com/SigNoz/zap_otlp/zap_otlp_sync v0.0.0-20230822164844-1b861a431974 github.com/SigNoz/zap_otlp/zap_otlp_sync v0.0.0-20230822164844-1b861a431974
github.com/antonmedv/expr v1.15.3 github.com/antonmedv/expr v1.15.3
@ -34,7 +34,6 @@ require (
github.com/oklog/oklog v0.3.2 github.com/oklog/oklog v0.3.2
github.com/open-telemetry/opamp-go v0.5.0 github.com/open-telemetry/opamp-go v0.5.0
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza v0.111.0 github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza v0.111.0
github.com/open-telemetry/opentelemetry-collector-contrib/processor/logstransformprocessor v0.111.0
github.com/opentracing/opentracing-go v1.2.0 github.com/opentracing/opentracing-go v1.2.0
github.com/patrickmn/go-cache v2.1.0+incompatible github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
@ -49,18 +48,9 @@ require (
github.com/soheilhy/cmux v0.1.5 github.com/soheilhy/cmux v0.1.5
github.com/srikanthccv/ClickHouse-go-mock v0.9.0 github.com/srikanthccv/ClickHouse-go-mock v0.9.0
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
go.opentelemetry.io/collector/component v0.111.0
go.opentelemetry.io/collector/confmap v1.17.0 go.opentelemetry.io/collector/confmap v1.17.0
go.opentelemetry.io/collector/confmap/converter/expandconverter v0.111.0
go.opentelemetry.io/collector/confmap/provider/fileprovider v1.17.0
go.opentelemetry.io/collector/consumer v0.111.0
go.opentelemetry.io/collector/consumer/consumertest v0.111.0
go.opentelemetry.io/collector/exporter v0.111.0
go.opentelemetry.io/collector/otelcol v0.111.0
go.opentelemetry.io/collector/pdata v1.17.0 go.opentelemetry.io/collector/pdata v1.17.0
go.opentelemetry.io/collector/processor v0.111.0 go.opentelemetry.io/collector/processor v0.111.0
go.opentelemetry.io/collector/receiver v0.111.0
go.opentelemetry.io/collector/service v0.111.0
go.opentelemetry.io/contrib/bridges/otelzap v0.0.0-20240820072021-3fab5f5f20fb go.opentelemetry.io/contrib/bridges/otelzap v0.0.0-20240820072021-3fab5f5f20fb
go.opentelemetry.io/contrib/config v0.10.0 go.opentelemetry.io/contrib/config v0.10.0
go.opentelemetry.io/otel v1.30.0 go.opentelemetry.io/otel v1.30.0
@ -179,12 +169,18 @@ require (
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opencensus.io v0.24.0 // indirect go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/collector v0.111.0 // indirect go.opentelemetry.io/collector v0.111.0 // indirect
go.opentelemetry.io/collector/component v0.111.0 // indirect
go.opentelemetry.io/collector/component/componentprofiles v0.111.0 // indirect go.opentelemetry.io/collector/component/componentprofiles v0.111.0 // indirect
go.opentelemetry.io/collector/component/componentstatus v0.111.0 // indirect go.opentelemetry.io/collector/component/componentstatus v0.111.0 // indirect
go.opentelemetry.io/collector/config/configtelemetry v0.111.0 // indirect go.opentelemetry.io/collector/config/configtelemetry v0.111.0 // indirect
go.opentelemetry.io/collector/confmap/converter/expandconverter v0.111.0 // indirect
go.opentelemetry.io/collector/confmap/provider/fileprovider v1.17.0 // indirect
go.opentelemetry.io/collector/connector v0.111.0 // indirect go.opentelemetry.io/collector/connector v0.111.0 // indirect
go.opentelemetry.io/collector/connector/connectorprofiles v0.111.0 // indirect go.opentelemetry.io/collector/connector/connectorprofiles v0.111.0 // indirect
go.opentelemetry.io/collector/consumer v0.111.0 // indirect
go.opentelemetry.io/collector/consumer/consumerprofiles v0.111.0 // indirect go.opentelemetry.io/collector/consumer/consumerprofiles v0.111.0 // indirect
go.opentelemetry.io/collector/consumer/consumertest v0.111.0 // indirect
go.opentelemetry.io/collector/exporter v0.111.0 // indirect
go.opentelemetry.io/collector/exporter/exporterprofiles v0.111.0 // indirect go.opentelemetry.io/collector/exporter/exporterprofiles v0.111.0 // indirect
go.opentelemetry.io/collector/extension v0.111.0 // indirect go.opentelemetry.io/collector/extension v0.111.0 // indirect
go.opentelemetry.io/collector/extension/experimental/storage v0.111.0 // indirect go.opentelemetry.io/collector/extension/experimental/storage v0.111.0 // indirect
@ -192,12 +188,15 @@ require (
go.opentelemetry.io/collector/featuregate v1.17.0 // indirect go.opentelemetry.io/collector/featuregate v1.17.0 // indirect
go.opentelemetry.io/collector/internal/globalgates v0.111.0 // indirect go.opentelemetry.io/collector/internal/globalgates v0.111.0 // indirect
go.opentelemetry.io/collector/internal/globalsignal v0.111.0 // indirect go.opentelemetry.io/collector/internal/globalsignal v0.111.0 // indirect
go.opentelemetry.io/collector/otelcol v0.111.0 // indirect
go.opentelemetry.io/collector/pdata/pprofile v0.111.0 // indirect go.opentelemetry.io/collector/pdata/pprofile v0.111.0 // indirect
go.opentelemetry.io/collector/pdata/testdata v0.111.0 // indirect go.opentelemetry.io/collector/pdata/testdata v0.111.0 // indirect
go.opentelemetry.io/collector/pipeline v0.111.0 // indirect go.opentelemetry.io/collector/pipeline v0.111.0 // indirect
go.opentelemetry.io/collector/processor/processorprofiles v0.111.0 // indirect go.opentelemetry.io/collector/processor/processorprofiles v0.111.0 // indirect
go.opentelemetry.io/collector/receiver v0.111.0 // indirect
go.opentelemetry.io/collector/receiver/receiverprofiles v0.111.0 // indirect go.opentelemetry.io/collector/receiver/receiverprofiles v0.111.0 // indirect
go.opentelemetry.io/collector/semconv v0.111.0 // indirect go.opentelemetry.io/collector/semconv v0.111.0 // indirect
go.opentelemetry.io/collector/service v0.111.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.30.0 // indirect go.opentelemetry.io/contrib/propagators/b3 v1.30.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.6.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.6.0 // indirect

4
go.sum
View File

@ -70,8 +70,8 @@ github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd h1:Bk43AsDYe0fhkb
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd/go.mod h1:nxRcH/OEdM8QxzH37xkGzomr1O0JpYBRS6pwjsWW6Pc= github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd/go.mod h1:nxRcH/OEdM8QxzH37xkGzomr1O0JpYBRS6pwjsWW6Pc=
github.com/SigNoz/prometheus v1.12.0 h1:+BXeIHyMOOWWa+xjhJ+x80JFva7r1WzWIfIhQ5PUmIE= github.com/SigNoz/prometheus v1.12.0 h1:+BXeIHyMOOWWa+xjhJ+x80JFva7r1WzWIfIhQ5PUmIE=
github.com/SigNoz/prometheus v1.12.0/go.mod h1:EqNM27OwmPfqMUk+E+XG1L9rfDFcyXnzzDrg0EPOfxA= github.com/SigNoz/prometheus v1.12.0/go.mod h1:EqNM27OwmPfqMUk+E+XG1L9rfDFcyXnzzDrg0EPOfxA=
github.com/SigNoz/signoz-otel-collector v0.111.9 h1:U4q7o0H9C842zQFsM5wGHgt9ba92KU6LxnKmmYQ91ew= github.com/SigNoz/signoz-otel-collector v0.111.13 h1:pWaRrt4mGQyl2DZUYRTr9A+KnvcCeGBwX65PFV2duMg=
github.com/SigNoz/signoz-otel-collector v0.111.9/go.mod h1:vRDT10om89DHybN7SRMlt8IN9+/pgh1D57pNHPr2LM4= github.com/SigNoz/signoz-otel-collector v0.111.13/go.mod h1:vRDT10om89DHybN7SRMlt8IN9+/pgh1D57pNHPr2LM4=
github.com/SigNoz/zap_otlp v0.1.0 h1:T7rRcFN87GavY8lDGZj0Z3Xv6OhJA6Pj3I9dNPmqvRc= github.com/SigNoz/zap_otlp v0.1.0 h1:T7rRcFN87GavY8lDGZj0Z3Xv6OhJA6Pj3I9dNPmqvRc=
github.com/SigNoz/zap_otlp v0.1.0/go.mod h1:lcHvbDbRgvDnPxo9lDlaL1JK2PyOyouP/C3ynnYIvyo= github.com/SigNoz/zap_otlp v0.1.0/go.mod h1:lcHvbDbRgvDnPxo9lDlaL1JK2PyOyouP/C3ynnYIvyo=
github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974 h1:PKVgdf83Yw+lZJbFtNGBgqXiXNf3+kOXW2qZ7Ms7OaY= github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974 h1:PKVgdf83Yw+lZJbFtNGBgqXiXNf3+kOXW2qZ7Ms7OaY=

View File

@ -22,6 +22,7 @@ const (
defaultTraceDB string = "signoz_traces" defaultTraceDB string = "signoz_traces"
defaultOperationsTable string = "distributed_signoz_operations" defaultOperationsTable string = "distributed_signoz_operations"
defaultIndexTable string = "distributed_signoz_index_v2" defaultIndexTable string = "distributed_signoz_index_v2"
defaultLocalIndexTable string = "signoz_index_v2"
defaultErrorTable string = "distributed_signoz_error_index_v2" defaultErrorTable string = "distributed_signoz_error_index_v2"
defaultDurationTable string = "distributed_durationSort" defaultDurationTable string = "distributed_durationSort"
defaultUsageExplorerTable string = "distributed_usage_explorer" defaultUsageExplorerTable string = "distributed_usage_explorer"
@ -45,6 +46,11 @@ const (
defaultLogsTableV2 string = "distributed_logs_v2" defaultLogsTableV2 string = "distributed_logs_v2"
defaultLogsResourceLocalTableV2 string = "logs_v2_resource" defaultLogsResourceLocalTableV2 string = "logs_v2_resource"
defaultLogsResourceTableV2 string = "distributed_logs_v2_resource" defaultLogsResourceTableV2 string = "distributed_logs_v2_resource"
defaultTraceIndexTableV3 string = "distributed_signoz_index_v3"
defaultTraceLocalTableName string = "signoz_index_v3"
defaultTraceResourceTableV3 string = "distributed_traces_v3_resource"
defaultTraceSummaryTable string = "distributed_trace_summary"
) )
// NamespaceConfig is Clickhouse's internal configuration data // NamespaceConfig is Clickhouse's internal configuration data
@ -58,6 +64,7 @@ type namespaceConfig struct {
TraceDB string TraceDB string
OperationsTable string OperationsTable string
IndexTable string IndexTable string
LocalIndexTable string
DurationTable string DurationTable string
UsageExplorerTable string UsageExplorerTable string
SpansTable string SpansTable string
@ -82,6 +89,11 @@ type namespaceConfig struct {
LogsTableV2 string LogsTableV2 string
LogsResourceLocalTableV2 string LogsResourceLocalTableV2 string
LogsResourceTableV2 string LogsResourceTableV2 string
TraceIndexTableV3 string
TraceLocalTableNameV3 string
TraceResourceTableV3 string
TraceSummaryTable string
} }
// Connecto defines how to connect to the database // Connecto defines how to connect to the database
@ -150,6 +162,7 @@ func NewOptions(
TraceDB: defaultTraceDB, TraceDB: defaultTraceDB,
OperationsTable: defaultOperationsTable, OperationsTable: defaultOperationsTable,
IndexTable: defaultIndexTable, IndexTable: defaultIndexTable,
LocalIndexTable: defaultLocalIndexTable,
ErrorTable: defaultErrorTable, ErrorTable: defaultErrorTable,
DurationTable: defaultDurationTable, DurationTable: defaultDurationTable,
UsageExplorerTable: defaultUsageExplorerTable, UsageExplorerTable: defaultUsageExplorerTable,
@ -174,6 +187,11 @@ func NewOptions(
LogsLocalTableV2: defaultLogsLocalTableV2, LogsLocalTableV2: defaultLogsLocalTableV2,
LogsResourceTableV2: defaultLogsResourceTableV2, LogsResourceTableV2: defaultLogsResourceTableV2,
LogsResourceLocalTableV2: defaultLogsResourceLocalTableV2, LogsResourceLocalTableV2: defaultLogsResourceLocalTableV2,
TraceIndexTableV3: defaultTraceIndexTableV3,
TraceLocalTableNameV3: defaultTraceLocalTableName,
TraceResourceTableV3: defaultTraceResourceTableV3,
TraceSummaryTable: defaultTraceSummaryTable,
}, },
others: make(map[string]*namespaceConfig, len(otherNamespaces)), others: make(map[string]*namespaceConfig, len(otherNamespaces)),
} }

File diff suppressed because it is too large Load Diff

View File

@ -468,6 +468,7 @@ func GetDashboardsInfo(ctx context.Context) (*model.DashboardsInfo, error) {
dashboardsInfo.MetricBasedPanels += dashboardsInfo.MetricBasedPanels dashboardsInfo.MetricBasedPanels += dashboardsInfo.MetricBasedPanels
dashboardsInfo.LogsPanelsWithAttrContainsOp += dashboardInfo.LogsPanelsWithAttrContainsOp dashboardsInfo.LogsPanelsWithAttrContainsOp += dashboardInfo.LogsPanelsWithAttrContainsOp
dashboardsInfo.DashboardsWithLogsChQuery += dashboardInfo.DashboardsWithLogsChQuery dashboardsInfo.DashboardsWithLogsChQuery += dashboardInfo.DashboardsWithLogsChQuery
dashboardsInfo.DashboardsWithTraceChQuery += dashboardInfo.DashboardsWithTraceChQuery
if isDashboardWithTSV2(dashboard.Data) { if isDashboardWithTSV2(dashboard.Data) {
count = count + 1 count = count + 1
} }
@ -499,6 +500,18 @@ func isDashboardWithLogsClickhouseQuery(data map[string]interface{}) bool {
return result return result
} }
func isDashboardWithTracesClickhouseQuery(data map[string]interface{}) bool {
jsonData, err := json.Marshal(data)
if err != nil {
return false
}
str := string(jsonData)
result := strings.Contains(str, "signoz_traces.distributed_signoz_index_v2") ||
strings.Contains(str, "signoz_traces.distributed_signoz_spans") ||
strings.Contains(str, "signoz_traces.distributed_signoz_error_index_v2")
return result
}
func isDashboardWithPanelAndName(data map[string]interface{}) bool { func isDashboardWithPanelAndName(data map[string]interface{}) bool {
isDashboardName := false isDashboardName := false
isDashboardWithPanelAndName := false isDashboardWithPanelAndName := false
@ -559,7 +572,9 @@ func checkLogPanelAttrContains(data map[string]interface{}) int {
func countPanelsInDashboard(inputData map[string]interface{}) model.DashboardsInfo { func countPanelsInDashboard(inputData map[string]interface{}) model.DashboardsInfo {
var logsPanelCount, tracesPanelCount, metricsPanelCount, logsPanelsWithAttrContains int var logsPanelCount, tracesPanelCount, metricsPanelCount, logsPanelsWithAttrContains int
var logChQuery bool traceChQueryCount := 0
logChQueryCount := 0
// totalPanels := 0 // totalPanels := 0
if inputData != nil && inputData["widgets"] != nil { if inputData != nil && inputData["widgets"] != nil {
widgets, ok := inputData["widgets"] widgets, ok := inputData["widgets"]
@ -593,7 +608,10 @@ func countPanelsInDashboard(inputData map[string]interface{}) model.DashboardsIn
} }
} else if ok && query["queryType"] == "clickhouse_sql" && query["clickhouse_sql"] != nil { } else if ok && query["queryType"] == "clickhouse_sql" && query["clickhouse_sql"] != nil {
if isDashboardWithLogsClickhouseQuery(inputData) { if isDashboardWithLogsClickhouseQuery(inputData) {
logChQuery = true logChQueryCount = 1
}
if isDashboardWithTracesClickhouseQuery(inputData) {
traceChQueryCount = 1
} }
} }
} }
@ -602,16 +620,13 @@ func countPanelsInDashboard(inputData map[string]interface{}) model.DashboardsIn
} }
} }
logChQueryCount := 0
if logChQuery {
logChQueryCount = 1
}
return model.DashboardsInfo{ return model.DashboardsInfo{
LogsBasedPanels: logsPanelCount, LogsBasedPanels: logsPanelCount,
TracesBasedPanels: tracesPanelCount, TracesBasedPanels: tracesPanelCount,
MetricBasedPanels: metricsPanelCount, MetricBasedPanels: metricsPanelCount,
DashboardsWithLogsChQuery: logChQueryCount, DashboardsWithLogsChQuery: logChQueryCount,
DashboardsWithTraceChQuery: traceChQueryCount,
LogsPanelsWithAttrContainsOp: logsPanelsWithAttrContains, LogsPanelsWithAttrContainsOp: logsPanelsWithAttrContains,
} }
} }

View File

@ -39,6 +39,7 @@ import (
querierV2 "go.signoz.io/signoz/pkg/query-service/app/querier/v2" querierV2 "go.signoz.io/signoz/pkg/query-service/app/querier/v2"
"go.signoz.io/signoz/pkg/query-service/app/queryBuilder" "go.signoz.io/signoz/pkg/query-service/app/queryBuilder"
tracesV3 "go.signoz.io/signoz/pkg/query-service/app/traces/v3" tracesV3 "go.signoz.io/signoz/pkg/query-service/app/traces/v3"
tracesV4 "go.signoz.io/signoz/pkg/query-service/app/traces/v4"
"go.signoz.io/signoz/pkg/query-service/auth" "go.signoz.io/signoz/pkg/query-service/auth"
"go.signoz.io/signoz/pkg/query-service/cache" "go.signoz.io/signoz/pkg/query-service/cache"
"go.signoz.io/signoz/pkg/query-service/common" "go.signoz.io/signoz/pkg/query-service/common"
@ -111,6 +112,7 @@ type APIHandler struct {
Upgrader *websocket.Upgrader Upgrader *websocket.Upgrader
UseLogsNewSchema bool UseLogsNewSchema bool
UseTraceNewSchema bool
UseLicensesV3 bool UseLicensesV3 bool
hostsRepo *inframetrics.HostsRepo hostsRepo *inframetrics.HostsRepo
@ -163,6 +165,7 @@ type APIHandlerOpts struct {
// Use Logs New schema // Use Logs New schema
UseLogsNewSchema bool UseLogsNewSchema bool
UseTraceNewSchema bool
// Use Licenses V3 structure // Use Licenses V3 structure
UseLicensesV3 bool UseLicensesV3 bool
} }
@ -182,6 +185,7 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) {
FluxInterval: opts.FluxInterval, FluxInterval: opts.FluxInterval,
FeatureLookup: opts.FeatureFlags, FeatureLookup: opts.FeatureFlags,
UseLogsNewSchema: opts.UseLogsNewSchema, UseLogsNewSchema: opts.UseLogsNewSchema,
UseTraceNewSchema: opts.UseTraceNewSchema,
} }
querierOptsV2 := querierV2.QuerierOptions{ querierOptsV2 := querierV2.QuerierOptions{
@ -191,6 +195,7 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) {
FluxInterval: opts.FluxInterval, FluxInterval: opts.FluxInterval,
FeatureLookup: opts.FeatureFlags, FeatureLookup: opts.FeatureFlags,
UseLogsNewSchema: opts.UseLogsNewSchema, UseLogsNewSchema: opts.UseLogsNewSchema,
UseTraceNewSchema: opts.UseTraceNewSchema,
} }
querier := querier.NewQuerier(querierOpts) querier := querier.NewQuerier(querierOpts)
@ -224,6 +229,7 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) {
querier: querier, querier: querier,
querierV2: querierv2, querierV2: querierv2,
UseLogsNewSchema: opts.UseLogsNewSchema, UseLogsNewSchema: opts.UseLogsNewSchema,
UseTraceNewSchema: opts.UseTraceNewSchema,
UseLicensesV3: opts.UseLicensesV3, UseLicensesV3: opts.UseLicensesV3,
hostsRepo: hostsRepo, hostsRepo: hostsRepo,
processesRepo: processesRepo, processesRepo: processesRepo,
@ -242,9 +248,14 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) {
logsQueryBuilder = logsv4.PrepareLogsQuery logsQueryBuilder = logsv4.PrepareLogsQuery
} }
tracesQueryBuilder := tracesV3.PrepareTracesQuery
if opts.UseTraceNewSchema {
tracesQueryBuilder = tracesV4.PrepareTracesQuery
}
builderOpts := queryBuilder.QueryBuilderOptions{ builderOpts := queryBuilder.QueryBuilderOptions{
BuildMetricQuery: metricsv3.PrepareMetricQuery, BuildMetricQuery: metricsv3.PrepareMetricQuery,
BuildTraceQuery: tracesV3.PrepareTracesQuery, BuildTraceQuery: tracesQueryBuilder,
BuildLogQuery: logsQueryBuilder, BuildLogQuery: logsQueryBuilder,
} }
aH.queryBuilder = queryBuilder.NewQueryBuilder(builderOpts, aH.featureFlags) aH.queryBuilder = queryBuilder.NewQueryBuilder(builderOpts, aH.featureFlags)
@ -508,7 +519,6 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *AuthMiddleware) {
// router.HandleFunc("/api/v1/get_percentiles", aH.getApplicationPercentiles).Methods(http.MethodGet) // router.HandleFunc("/api/v1/get_percentiles", aH.getApplicationPercentiles).Methods(http.MethodGet)
router.HandleFunc("/api/v1/services", am.ViewAccess(aH.getServices)).Methods(http.MethodPost) router.HandleFunc("/api/v1/services", am.ViewAccess(aH.getServices)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/services/list", am.ViewAccess(aH.getServicesList)).Methods(http.MethodGet) router.HandleFunc("/api/v1/services/list", am.ViewAccess(aH.getServicesList)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/service/overview", am.ViewAccess(aH.getServiceOverview)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/service/top_operations", am.ViewAccess(aH.getTopOperations)).Methods(http.MethodPost) router.HandleFunc("/api/v1/service/top_operations", am.ViewAccess(aH.getTopOperations)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/service/top_level_operations", am.ViewAccess(aH.getServicesTopLevelOps)).Methods(http.MethodPost) router.HandleFunc("/api/v1/service/top_level_operations", am.ViewAccess(aH.getServicesTopLevelOps)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/traces/{traceId}", am.ViewAccess(aH.SearchTraces)).Methods(http.MethodGet) router.HandleFunc("/api/v1/traces/{traceId}", am.ViewAccess(aH.SearchTraces)).Methods(http.MethodGet)
@ -1632,22 +1642,6 @@ func (aH *APIHandler) getUsage(w http.ResponseWriter, r *http.Request) {
} }
func (aH *APIHandler) getServiceOverview(w http.ResponseWriter, r *http.Request) {
query, err := parseGetServiceOverviewRequest(r)
if aH.HandleError(w, err, http.StatusBadRequest) {
return
}
result, apiErr := aH.reader.GetServiceOverview(r.Context(), query, aH.skipConfig)
if apiErr != nil && aH.HandleError(w, apiErr.Err, http.StatusInternalServerError) {
return
}
aH.WriteJSON(w, r, result)
}
func (aH *APIHandler) getServicesTopLevelOps(w http.ResponseWriter, r *http.Request) { func (aH *APIHandler) getServicesTopLevelOps(w http.ResponseWriter, r *http.Request) {
var start, end time.Time var start, end time.Time
@ -4347,9 +4341,14 @@ func (aH *APIHandler) queryRangeV3(ctx context.Context, queryRangeParams *v3.Que
RespondError(w, apiErrObj, errQuriesByName) RespondError(w, apiErrObj, errQuriesByName)
return return
} }
if aH.UseTraceNewSchema {
tracesV4.Enrich(queryRangeParams, spanKeys)
} else {
tracesV3.Enrich(queryRangeParams, spanKeys) tracesV3.Enrich(queryRangeParams, spanKeys)
} }
}
// WARN: Only works for AND operator in traces query // WARN: Only works for AND operator in traces query
if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeBuilder { if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeBuilder {
// check if traceID is used as filter (with equal/similar operator) in traces query if yes add timestamp filter to queryRange params // check if traceID is used as filter (with equal/similar operator) in traces query if yes add timestamp filter to queryRange params
@ -4817,8 +4816,12 @@ func (aH *APIHandler) queryRangeV4(ctx context.Context, queryRangeParams *v3.Que
RespondError(w, apiErrObj, errQuriesByName) RespondError(w, apiErrObj, errQuriesByName)
return return
} }
if aH.UseTraceNewSchema {
tracesV4.Enrich(queryRangeParams, spanKeys)
} else {
tracesV3.Enrich(queryRangeParams, spanKeys) tracesV3.Enrich(queryRangeParams, spanKeys)
} }
}
// WARN: Only works for AND operator in traces query // WARN: Only works for AND operator in traces query
if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeBuilder { if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeBuilder {

View File

@ -2,6 +2,7 @@ package inframetrics
import ( import (
"context" "context"
"fmt"
"math" "math"
"sort" "sort"
"strings" "strings"
@ -9,6 +10,7 @@ import (
"go.signoz.io/signoz/pkg/query-service/app/metrics/v4/helpers" "go.signoz.io/signoz/pkg/query-service/app/metrics/v4/helpers"
"go.signoz.io/signoz/pkg/query-service/common" "go.signoz.io/signoz/pkg/query-service/common"
"go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/query-service/interfaces" "go.signoz.io/signoz/pkg/query-service/interfaces"
"go.signoz.io/signoz/pkg/query-service/model" "go.signoz.io/signoz/pkg/query-service/model"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3" v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
@ -310,6 +312,45 @@ func (h *HostsRepo) getTopHostGroups(ctx context.Context, req model.HostListRequ
return topHostGroups, allHostGroups, nil return topHostGroups, allHostGroups, nil
} }
func (h *HostsRepo) DidSendHostMetricsData(ctx context.Context, req model.HostListRequest) (bool, error) {
names := []string{}
for _, metricName := range metricNamesForHosts {
names = append(names, metricName)
}
namesStr := "'" + strings.Join(names, "','") + "'"
query := fmt.Sprintf("SELECT count() FROM %s.%s WHERE metric_name IN (%s)",
constants.SIGNOZ_METRIC_DBNAME, constants.SIGNOZ_TIMESERIES_v4_1DAY_TABLENAME, namesStr)
count, err := h.reader.GetCountOfThings(ctx, query)
if err != nil {
return false, err
}
return count > 0, nil
}
func (h *HostsRepo) IsSendingK8SAgentMetrics(ctx context.Context, req model.HostListRequest) (bool, error) {
names := []string{}
for _, metricName := range metricNamesForHosts {
names = append(names, metricName)
}
namesStr := "'" + strings.Join(names, "','") + "'"
query := fmt.Sprintf(`
SELECT count()
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)
count, err := h.reader.GetCountOfThings(ctx, query)
return count > 0, err
}
func (h *HostsRepo) GetHostList(ctx context.Context, req model.HostListRequest) (model.HostListResponse, error) { func (h *HostsRepo) GetHostList(ctx context.Context, req model.HostListRequest) (model.HostListResponse, error) {
resp := model.HostListResponse{} resp := model.HostListResponse{}
@ -330,6 +371,14 @@ func (h *HostsRepo) GetHostList(ctx context.Context, req model.HostListRequest)
resp.Type = model.ResponseTypeGroupedList resp.Type = model.ResponseTypeGroupedList
} }
// 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 sentAnyHostMetricsData, err := h.DidSendHostMetricsData(ctx, req); err == nil {
resp.SentAnyHostMetricsData = sentAnyHostMetricsData
}
step := int64(math.Max(float64(common.MinAllowedStepInterval(req.Start, req.End)), 60)) step := int64(math.Max(float64(common.MinAllowedStepInterval(req.Start, req.End)), 60))
query := HostsTableListQuery.Clone() query := HostsTableListQuery.Clone()
@ -439,6 +488,7 @@ func (h *HostsRepo) GetHostList(ctx context.Context, req model.HostListRequest)
} }
resp.Total = len(allHostGroups) resp.Total = len(allHostGroups)
resp.Records = records resp.Records = records
resp.SortBy(req.OrderBy)
return resp, nil return resp, nil
} }

View File

@ -6,13 +6,13 @@ import (
"strings" "strings"
"time" "time"
"github.com/SigNoz/signoz-otel-collector/pkg/collectorsimulator"
_ "github.com/SigNoz/signoz-otel-collector/pkg/parser/grok" _ "github.com/SigNoz/signoz-otel-collector/pkg/parser/grok"
"github.com/SigNoz/signoz-otel-collector/processor/signozlogspipelineprocessor" "github.com/SigNoz/signoz-otel-collector/processor/signozlogspipelineprocessor"
"github.com/pkg/errors" "github.com/pkg/errors"
"go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/pcommon"
"go.opentelemetry.io/collector/pdata/plog" "go.opentelemetry.io/collector/pdata/plog"
"go.opentelemetry.io/collector/processor" "go.opentelemetry.io/collector/processor"
"go.signoz.io/signoz/pkg/query-service/collectorsimulator"
"go.signoz.io/signoz/pkg/query-service/model" "go.signoz.io/signoz/pkg/query-service/model"
) )
@ -66,14 +66,20 @@ func SimulatePipelinesProcessing(
return updatedConf, nil return updatedConf, nil
} }
outputPLogs, collectorErrs, apiErr := collectorsimulator.SimulateLogsProcessing( outputPLogs, collectorErrs, simulationErr := collectorsimulator.SimulateLogsProcessing(
ctx, ctx,
processorFactories, processorFactories,
configGenerator, configGenerator,
simulatorInputPLogs, simulatorInputPLogs,
timeout, timeout,
) )
if apiErr != nil { if simulationErr != nil {
if errors.Is(simulationErr, collectorsimulator.ErrInvalidConfig) {
apiErr = model.BadRequest(simulationErr)
} else {
apiErr = model.InternalError(simulationErr)
}
return nil, collectorErrs, model.WrapApiError(apiErr, return nil, collectorErrs, model.WrapApiError(apiErr,
"could not simulate log pipelines processing.\nCollector errors", "could not simulate log pipelines processing.\nCollector errors",
) )

View File

@ -18,7 +18,7 @@ func EnrichmentRequired(params *v3.QueryRangeParamsV3) bool {
// Build queries for each builder query // Build queries for each builder query
for queryName, query := range compositeQuery.BuilderQueries { for queryName, query := range compositeQuery.BuilderQueries {
if query.Expression != queryName && query.DataSource != v3.DataSourceLogs { if query.Expression != queryName || query.DataSource != v3.DataSourceLogs {
continue continue
} }

View File

@ -195,28 +195,6 @@ func parseGetUsageRequest(r *http.Request) (*model.GetUsageParams, error) {
} }
func parseGetServiceOverviewRequest(r *http.Request) (*model.GetServiceOverviewParams, error) {
var postData *model.GetServiceOverviewParams
err := json.NewDecoder(r.Body).Decode(&postData)
if err != nil {
return nil, err
}
postData.Start, err = parseTimeStr(postData.StartTime, "start")
if err != nil {
return nil, err
}
postData.End, err = parseTimeMinusBufferStr(postData.EndTime, "end")
if err != nil {
return nil, err
}
postData.Period = fmt.Sprintf("PT%dM", postData.StepSeconds/60)
return postData, nil
}
func parseGetServicesRequest(r *http.Request) (*model.GetServicesParams, error) { func parseGetServicesRequest(r *http.Request) (*model.GetServicesParams, error) {
var postData *model.GetServicesParams var postData *model.GetServicesParams
@ -289,229 +267,6 @@ func DoesExistInSlice(item string, list []string) bool {
} }
return false return false
} }
func parseSpanFilterRequestBody(r *http.Request) (*model.SpanFilterParams, error) {
var postData *model.SpanFilterParams
err := json.NewDecoder(r.Body).Decode(&postData)
if err != nil {
return nil, err
}
postData.Start, err = parseTimeStr(postData.StartStr, "start")
if err != nil {
return nil, err
}
postData.End, err = parseTimeMinusBufferStr(postData.EndStr, "end")
if err != nil {
return nil, err
}
return postData, nil
}
func parseFilteredSpansRequest(r *http.Request, aH *APIHandler) (*model.GetFilteredSpansParams, error) {
var postData *model.GetFilteredSpansParams
err := json.NewDecoder(r.Body).Decode(&postData)
if err != nil {
return nil, err
}
postData.Start, err = parseTimeStr(postData.StartStr, "start")
if err != nil {
return nil, err
}
postData.End, err = parseTimeMinusBufferStr(postData.EndStr, "end")
if err != nil {
return nil, err
}
if postData.Limit == 0 {
postData.Limit = 10
}
if len(postData.Order) != 0 {
if postData.Order != baseconstants.Ascending && postData.Order != baseconstants.Descending {
return nil, errors.New("order param is not in correct format")
}
if postData.OrderParam != baseconstants.Duration && postData.OrderParam != baseconstants.Timestamp {
return nil, errors.New("order param is not in correct format")
}
if postData.OrderParam == baseconstants.Duration && !aH.CheckFeature(baseconstants.DurationSort) {
return nil, model.ErrFeatureUnavailable{Key: baseconstants.DurationSort}
} else if postData.OrderParam == baseconstants.Timestamp && !aH.CheckFeature(baseconstants.TimestampSort) {
return nil, model.ErrFeatureUnavailable{Key: baseconstants.TimestampSort}
}
}
tags, err := extractTagKeys(postData.Tags)
if err != nil {
return nil, err
}
postData.Tags = tags
return postData, nil
}
func parseFilteredSpanAggregatesRequest(r *http.Request) (*model.GetFilteredSpanAggregatesParams, error) {
var postData *model.GetFilteredSpanAggregatesParams
err := json.NewDecoder(r.Body).Decode(&postData)
if err != nil {
return nil, err
}
postData.Start, err = parseTimeStr(postData.StartStr, "start")
if err != nil {
return nil, err
}
postData.End, err = parseTimeMinusBufferStr(postData.EndStr, "end")
if err != nil {
return nil, err
}
step := postData.StepSeconds
if step == 0 {
return nil, errors.New("step param missing in query")
}
function := postData.Function
if len(function) == 0 {
return nil, errors.New("function param missing in query")
} else {
if !DoesExistInSlice(function, allowedFunctions) {
return nil, fmt.Errorf("given function: %s is not allowed in query", function)
}
}
var dimension, aggregationOption string
switch function {
case "count":
dimension = "calls"
aggregationOption = "count"
case "ratePerSec":
dimension = "calls"
aggregationOption = "rate_per_sec"
case "avg":
dimension = "duration"
aggregationOption = "avg"
case "sum":
dimension = "duration"
aggregationOption = "sum"
case "p50":
dimension = "duration"
aggregationOption = "p50"
case "p90":
dimension = "duration"
aggregationOption = "p90"
case "p95":
dimension = "duration"
aggregationOption = "p95"
case "p99":
dimension = "duration"
aggregationOption = "p99"
case "min":
dimension = "duration"
aggregationOption = "min"
case "max":
dimension = "duration"
aggregationOption = "max"
}
postData.AggregationOption = aggregationOption
postData.Dimension = dimension
tags, err := extractTagKeys(postData.Tags)
if err != nil {
return nil, err
}
postData.Tags = tags
return postData, nil
}
func extractTagKeys(tags []model.TagQueryParam) ([]model.TagQueryParam, error) {
newTags := make([]model.TagQueryParam, 0)
if len(tags) != 0 {
for _, tag := range tags {
customStr := strings.Split(tag.Key, ".(")
if len(customStr) < 2 {
return nil, fmt.Errorf("TagKey param is not valid in query")
} else {
tag.Key = customStr[0]
}
if tag.Operator == model.ExistsOperator || tag.Operator == model.NotExistsOperator {
if customStr[1] == string(model.TagTypeString)+")" {
tag.StringValues = []string{" "}
} else if customStr[1] == string(model.TagTypeBool)+")" {
tag.BoolValues = []bool{true}
} else if customStr[1] == string(model.TagTypeNumber)+")" {
tag.NumberValues = []float64{0}
} else {
return nil, fmt.Errorf("TagKey param is not valid in query")
}
}
newTags = append(newTags, tag)
}
}
return newTags, nil
}
func parseTagFilterRequest(r *http.Request) (*model.TagFilterParams, error) {
var postData *model.TagFilterParams
err := json.NewDecoder(r.Body).Decode(&postData)
if err != nil {
return nil, err
}
postData.Start, err = parseTimeStr(postData.StartStr, "start")
if err != nil {
return nil, err
}
postData.End, err = parseTimeMinusBufferStr(postData.EndStr, "end")
if err != nil {
return nil, err
}
return postData, nil
}
func parseTagValueRequest(r *http.Request) (*model.TagFilterParams, error) {
var postData *model.TagFilterParams
err := json.NewDecoder(r.Body).Decode(&postData)
if err != nil {
return nil, err
}
if postData.TagKey == (model.TagKey{}) {
return nil, fmt.Errorf("TagKey param missing in query")
}
if postData.TagKey.Type != model.TagTypeString && postData.TagKey.Type != model.TagTypeBool && postData.TagKey.Type != model.TagTypeNumber {
return nil, fmt.Errorf("tag keys type %s is not supported", postData.TagKey.Type)
}
if postData.Limit == 0 {
postData.Limit = 100
}
postData.Start, err = parseTimeStr(postData.StartStr, "start")
if err != nil {
return nil, err
}
postData.End, err = parseTimeMinusBufferStr(postData.EndStr, "end")
if err != nil {
return nil, err
}
return postData, nil
}
func parseListErrorsRequest(r *http.Request) (*model.ListErrorsParams, error) { func parseListErrorsRequest(r *http.Request) (*model.ListErrorsParams, error) {
var allowedOrderParams = []string{"exceptionType", "exceptionCount", "firstSeen", "lastSeen", "serviceName"} var allowedOrderParams = []string{"exceptionType", "exceptionCount", "firstSeen", "lastSeen", "serviceName"}

View File

@ -10,6 +10,7 @@ import (
logsV4 "go.signoz.io/signoz/pkg/query-service/app/logs/v4" logsV4 "go.signoz.io/signoz/pkg/query-service/app/logs/v4"
metricsV3 "go.signoz.io/signoz/pkg/query-service/app/metrics/v3" metricsV3 "go.signoz.io/signoz/pkg/query-service/app/metrics/v3"
tracesV3 "go.signoz.io/signoz/pkg/query-service/app/traces/v3" tracesV3 "go.signoz.io/signoz/pkg/query-service/app/traces/v3"
tracesV4 "go.signoz.io/signoz/pkg/query-service/app/traces/v4"
"go.signoz.io/signoz/pkg/query-service/common" "go.signoz.io/signoz/pkg/query-service/common"
"go.signoz.io/signoz/pkg/query-service/constants" "go.signoz.io/signoz/pkg/query-service/constants"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3" v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
@ -158,11 +159,16 @@ func (q *querier) runBuilderQuery(
if builderQuery.DataSource == v3.DataSourceTraces { if builderQuery.DataSource == v3.DataSourceTraces {
tracesQueryBuilder := tracesV3.PrepareTracesQuery
if q.UseTraceNewSchema {
tracesQueryBuilder = tracesV4.PrepareTracesQuery
}
var query string var query string
var err error var err error
// for ts query with group by and limit form two queries // for ts query with group by and limit form two queries
if params.CompositeQuery.PanelType == v3.PanelTypeGraph && builderQuery.Limit > 0 && len(builderQuery.GroupBy) > 0 { if params.CompositeQuery.PanelType == v3.PanelTypeGraph && builderQuery.Limit > 0 && len(builderQuery.GroupBy) > 0 {
limitQuery, err := tracesV3.PrepareTracesQuery( limitQuery, err := tracesQueryBuilder(
start, start,
end, end,
params.CompositeQuery.PanelType, params.CompositeQuery.PanelType,
@ -173,7 +179,7 @@ func (q *querier) runBuilderQuery(
ch <- channelResult{Err: err, Name: queryName, Query: limitQuery, Series: nil} ch <- channelResult{Err: err, Name: queryName, Query: limitQuery, Series: nil}
return return
} }
placeholderQuery, err := tracesV3.PrepareTracesQuery( placeholderQuery, err := tracesQueryBuilder(
start, start,
end, end,
params.CompositeQuery.PanelType, params.CompositeQuery.PanelType,
@ -186,7 +192,7 @@ func (q *querier) runBuilderQuery(
} }
query = fmt.Sprintf(placeholderQuery, limitQuery) query = fmt.Sprintf(placeholderQuery, limitQuery)
} else { } else {
query, err = tracesV3.PrepareTracesQuery( query, err = tracesQueryBuilder(
start, start,
end, end,
params.CompositeQuery.PanelType, params.CompositeQuery.PanelType,

Some files were not shown because too many files have changed in this diff Show More