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

View File

@ -69,7 +69,7 @@ services:
- --storage.path=/data
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
command:
- "--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`
otel-collector:
container_name: signoz-otel-collector
image: signoz/signoz-otel-collector:0.111.9
image: signoz/signoz-otel-collector:0.111.13
command:
[
"--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`
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
command:
[
@ -201,7 +201,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.59.0}
image: signoz/frontend:${DOCKER_TAG:-0.60.0}
container_name: signoz-frontend
restart: on-failure
depends_on:
@ -213,7 +213,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
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
command:
- "sync"
@ -228,7 +228,7 @@ services:
# condition: service_healthy
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
command:
- "async"
@ -245,7 +245,7 @@ services:
# condition: service_healthy
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
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`
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
command:
[
@ -208,7 +208,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.59.0}
image: signoz/frontend:${DOCKER_TAG:-0.60.0}
container_name: signoz-frontend
restart: on-failure
depends_on:
@ -220,7 +220,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
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
command:
- "--dsn=tcp://clickhouse:9000"
@ -234,7 +234,7 @@ services:
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
command:
[

View File

@ -38,9 +38,10 @@ type APIHandlerOptions struct {
Cache cache.Cache
Gateway *httputil.ReverseProxy
// Querier Influx Interval
FluxInterval time.Duration
UseLogsNewSchema bool
UseLicensesV3 bool
FluxInterval time.Duration
UseLogsNewSchema bool
UseTraceNewSchema bool
UseLicensesV3 bool
}
type APIHandler struct {
@ -66,6 +67,7 @@ func NewAPIHandler(opts APIHandlerOptions) (*APIHandler, error) {
Cache: opts.Cache,
FluxInterval: opts.FluxInterval,
UseLogsNewSchema: opts.UseLogsNewSchema,
UseTraceNewSchema: opts.UseTraceNewSchema,
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) {
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
if err := json.NewDecoder(r.Body).Decode(&l); err != nil {

View File

@ -2,32 +2,31 @@ package api
import (
"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) {
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
}
ah.APIHandler.SearchTraces(w, r)
return
result, err := ah.opts.DataConnector.SearchTraces(r.Context(), searchTracesParams, db.SmartTraceAlgorithm)
if ah.HandleError(w, err, http.StatusBadRequest) {
return
}
// This is commented since this will be taken care by new trace API
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,
cluster string,
useLogsNewSchema bool,
useTraceNewSchema bool,
) *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{
conn: ch.GetConn(),
appdb: localDB,

View File

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

View File

@ -18,13 +18,15 @@ import (
// Repo is license repo. stores license keys in a secured DB
type Repo struct {
db *sqlx.DB
db *sqlx.DB
useLicensesV3 bool
}
// NewLicenseRepo initiates a new license repo
func NewLicenseRepo(db *sqlx.DB) Repo {
func NewLicenseRepo(db *sqlx.DB, useLicensesV3 bool) 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
}
// 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) {
func (r *Repo) GetActiveLicenseV2(ctx context.Context) (*model.License, *basemodel.ApiError) {
var err error
licenses := []model.License{}
@ -109,6 +109,31 @@ func (r *Repo) GetActiveLicense(ctx context.Context) (*model.License, *basemodel
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) {
var err error
licenses := []model.LicenseDB{}

View File

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

View File

@ -94,6 +94,7 @@ func main() {
var cluster string
var useLogsNewSchema bool
var useTraceNewSchema bool
var useLicensesV3 bool
var cacheConfigPath, fluxInterval string
var enableQueryServiceLogOTLPExport bool
@ -105,6 +106,7 @@ func main() {
var gatewayUrl string
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.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)")
@ -145,6 +147,7 @@ func main() {
Cluster: cluster,
GatewayUrl: gatewayUrl,
UseLogsNewSchema: useLogsNewSchema,
UseTraceNewSchema: useTraceNewSchema,
UseLicensesV3: useLicensesV3,
}

View File

@ -247,3 +247,24 @@ func NewLicenseV3WithIDAndKey(id string, key string, data map[string]interface{}
licenseDataWithIdAndKey["key"] = key
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.Reader,
opts.UseLogsNewSchema,
opts.UseTraceNewSchema,
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
)
@ -122,6 +123,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
opts.FF,
opts.Reader,
opts.UseLogsNewSchema,
opts.UseTraceNewSchema,
baserules.WithSendAlways(),
baserules.WithSendUnmatched(),
)

View File

@ -128,7 +128,7 @@
"uuid": "^8.3.2",
"web-vitals": "^0.2.4",
"webpack": "5.94.0",
"webpack-dev-server": "^4.15.1",
"webpack-dev-server": "^4.15.2",
"webpack-retry-chunk-load-plugin": "3.1.1",
"xstate": "^4.31.0"
},
@ -241,6 +241,7 @@
"semver": "7.5.4",
"xml2js": "0.5.0",
"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",
"DEFAULT": "Open source Observability Platform | SigNoz",
"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",
"edit": "Edit",
"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",
"ALERT_HISTORY": "SigNoz | Alert Rule History",
"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);
}
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 =

View File

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

View File

@ -15,6 +15,7 @@ import {
EditAlertChannelsAlerts,
EditRulesPage,
ErrorDetails,
InfrastructureMonitoring,
IngestionSettings,
InstalledIntegrations,
LicensePage,
@ -391,6 +392,13 @@ const routes: AppRoutes[] = [
key: 'MESSAGING_QUEUES_DETAIL',
isPrivate: true,
},
{
path: ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
exact: true,
component: InfrastructureMonitoring,
key: 'INFRASTRUCTURE_MONITORING_HOSTS',
isPrivate: true,
},
];
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 createQueryParams from 'lib/createQueryParams';
import { ErrorResponse, SuccessResponse } from 'types/api';
// ** Types
import { IGetAttributeKeysPayload } from 'types/api/queryBuilder/getAttributeKeys';
import {
BaseAutocompleteData,

View File

@ -120,7 +120,6 @@ function CustomTimePicker({
useEffect(() => {
const value = getSelectedTimeRangeLabel(selectedTime, selectedValue);
setSelectedTimePlaceholderValue(value);
}, [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} />
<RawLogContent
className="raw-log-content"
$isReadOnly={isReadOnly}
$isActiveLog={isActiveLog}
$isDarkMode={isDarkMode}

View File

@ -23,4 +23,5 @@ export enum FeatureKeys {
PREMIUM_SUPPORT = 'PREMIUM_SUPPORT',
QUERY_BUILDER_SEARCH_V2 = 'QUERY_BUILDER_SEARCH_V2',
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',
REMOVE_ALERT_RULE: 'REMOVE_ALERT_RULE',
DUPLICATE_ALERT_RULE: 'DUPLICATE_ALERT_RULE',
GET_HOST_LIST: 'GET_HOST_LIST',
UPDATE_ALERT_RULE: 'UPDATE_ALERT_RULE',
};

View File

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

View File

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

View File

@ -17,6 +17,7 @@ function Controls({
handleNavigateNext,
handleCountItemsPerPageChange,
isLogPanel = false,
showSizeChanger = true,
}: ControlsProps): JSX.Element | null {
const isNextAndPreviousDisabled = useMemo(
() => isLoading || countPerPage < 0 || totalCount === 0,
@ -52,20 +53,23 @@ function Controls({
>
Next <RightOutlined />
</Button>
<Select<Pagination['limit']>
style={defaultSelectStyle}
loading={isLoading}
value={countPerPage}
onChange={handleCountItemsPerPageChange}
getPopupContainer={popupContainer}
>
{perPageOptions.map((count) => (
<Select.Option
key={count}
value={count}
>{`${count} / page`}</Select.Option>
))}
</Select>
{showSizeChanger && (
<Select<Pagination['limit']>
style={defaultSelectStyle}
loading={isLoading}
value={countPerPage}
onChange={handleCountItemsPerPageChange}
getPopupContainer={popupContainer}
>
{perPageOptions.map((count) => (
<Select.Option
key={count}
value={count}
>{`${count} / page`}</Select.Option>
))}
</Select>
)}
</Container>
);
}
@ -74,6 +78,7 @@ Controls.defaultProps = {
offset: 0,
perPageOptions: DEFAULT_PER_PAGE_OPTIONS,
isLogPanel: false,
showSizeChanger: true,
};
export interface ControlsProps {
@ -86,6 +91,7 @@ export interface ControlsProps {
handleNavigateNext: () => void;
handleCountItemsPerPageChange: (value: Pagination['limit']) => void;
isLogPanel?: boolean;
showSizeChanger?: boolean;
}
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 { LOCALSTORAGE } from 'constants/localStorage';
import { QueryParams } from 'constants/query';
@ -8,7 +8,7 @@ import { DataSource } from 'types/common/queryBuilder';
import { SaveNewViewHandlerProps } from './types';
export const getRandomColor = (): ColorType => {
export const getRandomColor = (): string => {
const colorKeys = Object.keys(Color) as (keyof typeof Color)[];
const randomKey = colorKeys[Math.floor(Math.random() * colorKeys.length)];
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 onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
event.stopPropagation();
if (event.metaKey || event.ctrlKey) {
window.open(getLink(), '_blank');
} else {
@ -458,7 +459,11 @@ function DashboardsList(): JSX.Element {
placement="left"
overlayClassName="title-toolip"
>
<Link to={getLink()} className="title-link">
<Link
to={getLink()}
className="title-link"
onClick={(e): void => e.stopPropagation()}
>
<img
src={dashboard?.image || Base64Icons[0]}
alt="dashboard-image"

View File

@ -9,7 +9,7 @@ import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import Header from 'container/OnboardingContainer/common/Header/Header';
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 useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';

View File

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

View File

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

View File

@ -119,6 +119,22 @@ function SideNav({
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
}, [featureResponse.data]);

View File

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

View File

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

View File

@ -68,6 +68,9 @@ function DateTimeSelection({
showResetButton = false,
showOldExplorerCTA = false,
defaultRelativeTime = RelativeTimeMap['6hr'] as Time,
isModalTimeSelection = false,
onTimeChange,
modalSelectedInterval,
}: Props): JSX.Element {
const [formSelector] = Form.useForm();
@ -205,7 +208,6 @@ function DateTimeSelection({
return `${startString} - ${endString}`;
}
return timeInterval;
};
@ -219,6 +221,12 @@ function DateTimeSelection({
}
}, [selectedTime]);
useEffect(() => {
if (isModalTimeSelection && modalSelectedInterval === 'custom') {
setCustomDTPickerVisible(true);
}
}, [isModalTimeSelection, modalSelectedInterval]);
const getDefaultTime = (pathName: string): Time => {
const defaultSelectedOption = getDefaultOption(pathName);
@ -306,6 +314,15 @@ function DateTimeSelection({
const onSelectHandler = useCallback(
(value: Time | CustomTimeType): void => {
if (isModalTimeSelection) {
if (value === 'custom') {
setCustomDTPickerVisible(true);
setIsValidteRelativeTime(false);
return;
}
onTimeChange?.(value);
return;
}
if (value !== 'custom') {
setIsOpen(false);
updateTimeInterval(value);
@ -345,7 +362,9 @@ function DateTimeSelection({
[
initQueryBuilderData,
isLogsExplorerPage,
isModalTimeSelection,
location.pathname,
onTimeChange,
refreshButtonHidden,
stagedQuery,
updateLocalStorageForRoutes,
@ -364,9 +383,34 @@ function DateTimeSelection({
}
}, [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 => {
if (dateTimeRange !== null) {
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) {
const startTime = startTimeMoment;
const endTime = endTimeMoment;
@ -397,6 +441,10 @@ function DateTimeSelection({
};
const onValidCustomDateHandler = (dateTimeStr: CustomTimeType): void => {
if (isModalTimeSelection) {
onTimeChange?.(dateTimeStr);
return;
}
setIsOpen(false);
updateTimeInterval(dateTimeStr);
updateLocalStorageForRoutes(dateTimeStr);
@ -452,7 +500,6 @@ function DateTimeSelection({
if (OLD_RELATIVE_TIME_VALUES.indexOf(time) > -1) {
return convertOldTimeToNewValidCustomTimeFormat(time);
}
return time;
};
@ -656,7 +703,9 @@ function DateTimeSelection({
onError={(hasError: boolean): void => {
setHasSelectedTimeError(hasError);
}}
selectedTime={selectedTime}
selectedTime={
isModalTimeSelection ? (modalSelectedInterval as Time) : selectedTime
}
onValidCustomDateChange={(dateTime): void => {
onValidCustomDateHandler(dateTime.timeStr as CustomTimeType);
}}
@ -664,9 +713,9 @@ function DateTimeSelection({
setIsValidteRelativeTime(isValid);
}}
selectedValue={getInputLabel(
dayjs(minTime / 1000000),
dayjs(maxTime / 1000000),
selectedTime,
dayjs(isModalTimeSelection ? modalStartTime : minTime / 1000000),
dayjs(isModalTimeSelection ? modalEndTime : maxTime / 1000000),
isModalTimeSelection ? modalSelectedInterval : selectedTime,
)}
data-testid="dropDown"
items={options}
@ -722,6 +771,12 @@ interface DateTimeSelectionV2Props {
showOldExplorerCTA?: boolean;
showResetButton?: boolean;
defaultRelativeTime?: Time;
isModalTimeSelection?: boolean;
onTimeChange?: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
modalSelectedInterval?: Time;
}
DateTimeSelection.defaultProps = {
@ -730,6 +785,9 @@ DateTimeSelection.defaultProps = {
showRefreshText: true,
showResetButton: false,
defaultRelativeTime: RelativeTimeMap['6hr'] as Time,
isModalTimeSelection: false,
onTimeChange: (): void => {},
modalSelectedInterval: RelativeTimeMap['5m'] as Time,
};
interface DispatchProps {
updateTimeInterval: (
@ -741,8 +799,31 @@ interface DispatchProps {
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
{ isModalTimeSelection }: DateTimeSelectionV2Props,
): 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),
});

View File

@ -12,6 +12,7 @@ function TraceExplorerControls({
totalCount,
perPageOptions,
config,
showSizeChanger = true,
}: TraceExplorerControlsProps): JSX.Element | null {
const {
pagination,
@ -38,6 +39,7 @@ function TraceExplorerControls({
handleCountItemsPerPageChange={handleCountItemsPerPageChange}
handleNavigateNext={handleNavigateNext}
handleNavigatePrevious={handleNavigatePrevious}
showSizeChanger={showSizeChanger}
/>
</Container>
);
@ -52,6 +54,11 @@ type TraceExplorerControlsProps = Pick<
'isLoading' | 'totalCount' | 'perPageOptions'
> & {
config?: OptionsMenuConfig | null;
showSizeChanger?: boolean;
};
TraceExplorerControls.defaultProps = {
showSizeChanger: true,
};
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 { QueryDataV3 } from 'types/api/widgets/getQuery';
function BlockLink({
export function BlockLink({
children,
to,
openInNewTab,
}: {
children: React.ReactNode;
to: string;
openInNewTab: boolean;
}): any {
// Display block to make the whole cell clickable
return (
<Link to={to} style={{ display: 'block' }}>
<Link
to={to}
style={{ display: 'block' }}
target={openInNewTab ? '_blank' : '_self'}
>
{children}
</Link>
);
@ -53,7 +59,7 @@ export const getListColumns = (
? 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)}>
<BlockLink to={getTraceLink(item)} openInNewTab={false}>
<Typography.Text>{date}</Typography.Text>
</BlockLink>
);
@ -70,7 +76,7 @@ export const getListColumns = (
render: (value, item): JSX.Element => {
if (value === '') {
return (
<BlockLink to={getTraceLink(item)}>
<BlockLink to={getTraceLink(item)} openInNewTab={false}>
<Typography data-testid={key}>N/A</Typography>
</BlockLink>
);
@ -78,7 +84,7 @@ export const getListColumns = (
if (key === 'httpMethod' || key === 'responseStatusCode') {
return (
<BlockLink to={getTraceLink(item)}>
<BlockLink to={getTraceLink(item)} openInNewTab={false}>
<Tag data-testid={key} color="magenta">
{value}
</Tag>
@ -88,14 +94,14 @@ export const getListColumns = (
if (key === 'durationNano') {
return (
<BlockLink to={getTraceLink(item)}>
<BlockLink to={getTraceLink(item)} openInNewTab={false}>
<Typography data-testid={key}>{getMs(value)}ms</Typography>
</BlockLink>
);
}
return (
<BlockLink to={getTraceLink(item)}>
<BlockLink to={getTraceLink(item)} openInNewTab={false}>
<Typography data-testid={key}>{value}</Typography>
</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,
whereClauseConfig?: WhereClauseConfig,
shouldUseSuggestions?: boolean,
isInfraMonitoring?: boolean,
): IAutoComplete => {
const [searchValue, setSearchValue] = useState<string>('');
const [searchKey, setSearchKey] = useState<string>('');
@ -37,6 +38,7 @@ export const useAutoComplete = (
query,
searchKey,
shouldUseSuggestions,
isInfraMonitoring,
);
const [key, operator, result] = useSetCurrentKeyAndOperator(searchValue, keys);
@ -170,4 +172,5 @@ interface IAutoComplete {
searchKey: string;
key: string;
exampleQueries: TagFilter[];
isInfraMonitoring?: boolean;
}

View File

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

View File

@ -12,7 +12,7 @@ import {
} from 'container/TopNav/DateTimeSelectionV2/config';
import { Pagination } from 'hooks/queryPagination';
import { convertNewDataToOld } from 'lib/newQueryBuilder/convertNewDataToOld';
import { isEmpty, cloneDeep } from 'lodash-es';
import { isEmpty } from 'lodash-es';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@ -24,6 +24,7 @@ export async function GetMetricQueryRange(
version: string,
signal?: AbortSignal,
headers?: Record<string, string>,
isInfraMonitoring?: boolean,
): Promise<SuccessResponse<MetricRangePayloadProps>> {
const { legendMap, queryPayload } = prepareQueryRangePayload(props);
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
await waitFor(() =>
expect(
queryByText(
`Just a bit of patience, just a little bits enough ⎯ were getting your logs!`,
),
).not.toBeInTheDocument(),
expect(queryByText(`Retrieving your logs!`)).not.toBeInTheDocument(),
);
// check for no data state to not be present

View File

@ -3,7 +3,7 @@ import './MessagingQueueHealthCheck.styles.scss';
import { Button } from 'antd';
import cx from 'classnames';
import { useOnboardingStatus } from 'hooks/messagingQueue / onboarding/useOnboardingStatus';
import { useOnboardingStatus } from 'hooks/messagingQueue/useOnboardingStatus';
import { Bolt, FolderTree } from 'lucide-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'],
INTEGRATIONS: ['ADMIN', 'EDITOR', 'VIEWER'],
SERVICE_TOP_LEVEL_OPERATIONS: ['ADMIN', 'EDITOR', 'VIEWER'],
INFRASTRUCTURE_MONITORING_HOSTS: ['ADMIN', 'EDITOR', 'VIEWER'],
};

View File

@ -4081,13 +4081,6 @@
dependencies:
"@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":
version "2.0.4"
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"
debug "4"
http-proxy-middleware@3.0.3:
http-proxy-middleware@3.0.3, http-proxy-middleware@^2.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-3.0.3.tgz#dc1313c75bd00d81e103823802551ee30130ebd1"
integrity sha512-usY0HG5nyDUwtqpiZdETNbmKtw3QQ1jwYFZ9wi5iHzX2BcILwQKtYDJPo7XHTsu5Z0B2Hj3W9NNnbd+AjFWjqg==
@ -9371,17 +9364,6 @@ http-proxy-middleware@3.0.3:
is-plain-object "^5.0.0"
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:
version "1.18.1"
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"
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:
version "4.1.0"
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"
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"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
@ -16549,7 +16526,7 @@ webpack-cli@^4.9.2:
rechoir "^0.7.0"
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"
resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz#eb7b39281cbce10e104eb2b8bf2b63fce49a3517"
integrity sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==
@ -16596,10 +16573,10 @@ webpack-dev-server@*:
webpack-dev-middleware "^5.3.1"
ws "^8.13.0"
webpack-dev-server@^4.15.1:
version "4.15.1"
resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz#8944b29c12760b3a45bdaa70799b17cb91b03df7"
integrity sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==
webpack-dev-server@^4.15.2:
version "4.15.2"
resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz#9e0c70a42a012560860adb186986da1248333173"
integrity sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==
dependencies:
"@types/bonjour" "^3.5.9"
"@types/connect-history-api-fallback" "^1.3.5"
@ -16629,7 +16606,7 @@ webpack-dev-server@^4.15.1:
serve-index "^1.9.1"
sockjs "^0.3.24"
spdy "^4.0.2"
webpack-dev-middleware "^5.3.1"
webpack-dev-middleware "^5.3.4"
ws "^8.13.0"
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/DATA-DOG/go-sqlmock v1.5.2
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_sync v0.0.0-20230822164844-1b861a431974
github.com/antonmedv/expr v1.15.3
@ -34,7 +34,6 @@ require (
github.com/oklog/oklog v0.3.2
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/processor/logstransformprocessor v0.111.0
github.com/opentracing/opentracing-go v1.2.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pkg/errors v0.9.1
@ -49,18 +48,9 @@ require (
github.com/soheilhy/cmux v0.1.5
github.com/srikanthccv/ClickHouse-go-mock v0.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/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/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/config v0.10.0
go.opentelemetry.io/otel v1.30.0
@ -179,12 +169,18 @@ require (
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opencensus.io v0.24.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/componentstatus 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/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/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/extension 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/internal/globalgates 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/testdata 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/receiver 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/service v0.111.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/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/prometheus v1.12.0 h1:+BXeIHyMOOWWa+xjhJ+x80JFva7r1WzWIfIhQ5PUmIE=
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.9/go.mod h1:vRDT10om89DHybN7SRMlt8IN9+/pgh1D57pNHPr2LM4=
github.com/SigNoz/signoz-otel-collector v0.111.13 h1:pWaRrt4mGQyl2DZUYRTr9A+KnvcCeGBwX65PFV2duMg=
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/go.mod h1:lcHvbDbRgvDnPxo9lDlaL1JK2PyOyouP/C3ynnYIvyo=
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"
defaultOperationsTable string = "distributed_signoz_operations"
defaultIndexTable string = "distributed_signoz_index_v2"
defaultLocalIndexTable string = "signoz_index_v2"
defaultErrorTable string = "distributed_signoz_error_index_v2"
defaultDurationTable string = "distributed_durationSort"
defaultUsageExplorerTable string = "distributed_usage_explorer"
@ -45,6 +46,11 @@ const (
defaultLogsTableV2 string = "distributed_logs_v2"
defaultLogsResourceLocalTableV2 string = "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
@ -58,6 +64,7 @@ type namespaceConfig struct {
TraceDB string
OperationsTable string
IndexTable string
LocalIndexTable string
DurationTable string
UsageExplorerTable string
SpansTable string
@ -82,6 +89,11 @@ type namespaceConfig struct {
LogsTableV2 string
LogsResourceLocalTableV2 string
LogsResourceTableV2 string
TraceIndexTableV3 string
TraceLocalTableNameV3 string
TraceResourceTableV3 string
TraceSummaryTable string
}
// Connecto defines how to connect to the database
@ -150,6 +162,7 @@ func NewOptions(
TraceDB: defaultTraceDB,
OperationsTable: defaultOperationsTable,
IndexTable: defaultIndexTable,
LocalIndexTable: defaultLocalIndexTable,
ErrorTable: defaultErrorTable,
DurationTable: defaultDurationTable,
UsageExplorerTable: defaultUsageExplorerTable,
@ -174,6 +187,11 @@ func NewOptions(
LogsLocalTableV2: defaultLogsLocalTableV2,
LogsResourceTableV2: defaultLogsResourceTableV2,
LogsResourceLocalTableV2: defaultLogsResourceLocalTableV2,
TraceIndexTableV3: defaultTraceIndexTableV3,
TraceLocalTableNameV3: defaultTraceLocalTableName,
TraceResourceTableV3: defaultTraceResourceTableV3,
TraceSummaryTable: defaultTraceSummaryTable,
},
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.LogsPanelsWithAttrContainsOp += dashboardInfo.LogsPanelsWithAttrContainsOp
dashboardsInfo.DashboardsWithLogsChQuery += dashboardInfo.DashboardsWithLogsChQuery
dashboardsInfo.DashboardsWithTraceChQuery += dashboardInfo.DashboardsWithTraceChQuery
if isDashboardWithTSV2(dashboard.Data) {
count = count + 1
}
@ -499,6 +500,18 @@ func isDashboardWithLogsClickhouseQuery(data map[string]interface{}) bool {
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 {
isDashboardName := false
isDashboardWithPanelAndName := false
@ -559,7 +572,9 @@ func checkLogPanelAttrContains(data map[string]interface{}) int {
func countPanelsInDashboard(inputData map[string]interface{}) model.DashboardsInfo {
var logsPanelCount, tracesPanelCount, metricsPanelCount, logsPanelsWithAttrContains int
var logChQuery bool
traceChQueryCount := 0
logChQueryCount := 0
// totalPanels := 0
if inputData != nil && inputData["widgets"] != nil {
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 {
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{
LogsBasedPanels: logsPanelCount,
TracesBasedPanels: tracesPanelCount,
MetricBasedPanels: metricsPanelCount,
DashboardsWithLogsChQuery: logChQueryCount,
DashboardsWithTraceChQuery: traceChQueryCount,
LogsPanelsWithAttrContainsOp: logsPanelsWithAttrContains,
}
}

View File

@ -39,6 +39,7 @@ import (
querierV2 "go.signoz.io/signoz/pkg/query-service/app/querier/v2"
"go.signoz.io/signoz/pkg/query-service/app/queryBuilder"
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/cache"
"go.signoz.io/signoz/pkg/query-service/common"
@ -110,8 +111,9 @@ type APIHandler struct {
// Websocket connection upgrader
Upgrader *websocket.Upgrader
UseLogsNewSchema bool
UseLicensesV3 bool
UseLogsNewSchema bool
UseTraceNewSchema bool
UseLicensesV3 bool
hostsRepo *inframetrics.HostsRepo
processesRepo *inframetrics.ProcessesRepo
@ -163,6 +165,7 @@ type APIHandlerOpts struct {
// Use Logs New schema
UseLogsNewSchema bool
UseTraceNewSchema bool
// Use Licenses V3 structure
UseLicensesV3 bool
}
@ -176,21 +179,23 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) {
}
querierOpts := querier.QuerierOptions{
Reader: opts.Reader,
Cache: opts.Cache,
KeyGenerator: queryBuilder.NewKeyGenerator(),
FluxInterval: opts.FluxInterval,
FeatureLookup: opts.FeatureFlags,
UseLogsNewSchema: opts.UseLogsNewSchema,
Reader: opts.Reader,
Cache: opts.Cache,
KeyGenerator: queryBuilder.NewKeyGenerator(),
FluxInterval: opts.FluxInterval,
FeatureLookup: opts.FeatureFlags,
UseLogsNewSchema: opts.UseLogsNewSchema,
UseTraceNewSchema: opts.UseTraceNewSchema,
}
querierOptsV2 := querierV2.QuerierOptions{
Reader: opts.Reader,
Cache: opts.Cache,
KeyGenerator: queryBuilder.NewKeyGenerator(),
FluxInterval: opts.FluxInterval,
FeatureLookup: opts.FeatureFlags,
UseLogsNewSchema: opts.UseLogsNewSchema,
Reader: opts.Reader,
Cache: opts.Cache,
KeyGenerator: queryBuilder.NewKeyGenerator(),
FluxInterval: opts.FluxInterval,
FeatureLookup: opts.FeatureFlags,
UseLogsNewSchema: opts.UseLogsNewSchema,
UseTraceNewSchema: opts.UseTraceNewSchema,
}
querier := querier.NewQuerier(querierOpts)
@ -224,6 +229,7 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) {
querier: querier,
querierV2: querierv2,
UseLogsNewSchema: opts.UseLogsNewSchema,
UseTraceNewSchema: opts.UseTraceNewSchema,
UseLicensesV3: opts.UseLicensesV3,
hostsRepo: hostsRepo,
processesRepo: processesRepo,
@ -242,9 +248,14 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) {
logsQueryBuilder = logsv4.PrepareLogsQuery
}
tracesQueryBuilder := tracesV3.PrepareTracesQuery
if opts.UseTraceNewSchema {
tracesQueryBuilder = tracesV4.PrepareTracesQuery
}
builderOpts := queryBuilder.QueryBuilderOptions{
BuildMetricQuery: metricsv3.PrepareMetricQuery,
BuildTraceQuery: tracesV3.PrepareTracesQuery,
BuildTraceQuery: tracesQueryBuilder,
BuildLogQuery: logsQueryBuilder,
}
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/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/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_level_operations", am.ViewAccess(aH.getServicesTopLevelOps)).Methods(http.MethodPost)
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) {
var start, end time.Time
@ -4347,7 +4341,12 @@ func (aH *APIHandler) queryRangeV3(ctx context.Context, queryRangeParams *v3.Que
RespondError(w, apiErrObj, errQuriesByName)
return
}
tracesV3.Enrich(queryRangeParams, spanKeys)
if aH.UseTraceNewSchema {
tracesV4.Enrich(queryRangeParams, spanKeys)
} else {
tracesV3.Enrich(queryRangeParams, spanKeys)
}
}
// WARN: Only works for AND operator in traces query
@ -4817,7 +4816,11 @@ func (aH *APIHandler) queryRangeV4(ctx context.Context, queryRangeParams *v3.Que
RespondError(w, apiErrObj, errQuriesByName)
return
}
tracesV3.Enrich(queryRangeParams, spanKeys)
if aH.UseTraceNewSchema {
tracesV4.Enrich(queryRangeParams, spanKeys)
} else {
tracesV3.Enrich(queryRangeParams, spanKeys)
}
}
// WARN: Only works for AND operator in traces query

View File

@ -2,6 +2,7 @@ package inframetrics
import (
"context"
"fmt"
"math"
"sort"
"strings"
@ -9,6 +10,7 @@ import (
"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/constants"
"go.signoz.io/signoz/pkg/query-service/interfaces"
"go.signoz.io/signoz/pkg/query-service/model"
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
}
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) {
resp := model.HostListResponse{}
@ -330,6 +371,14 @@ func (h *HostsRepo) GetHostList(ctx context.Context, req model.HostListRequest)
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))
query := HostsTableListQuery.Clone()
@ -439,6 +488,7 @@ func (h *HostsRepo) GetHostList(ctx context.Context, req model.HostListRequest)
}
resp.Total = len(allHostGroups)
resp.Records = records
resp.SortBy(req.OrderBy)
return resp, nil
}

View File

@ -6,13 +6,13 @@ import (
"strings"
"time"
"github.com/SigNoz/signoz-otel-collector/pkg/collectorsimulator"
_ "github.com/SigNoz/signoz-otel-collector/pkg/parser/grok"
"github.com/SigNoz/signoz-otel-collector/processor/signozlogspipelineprocessor"
"github.com/pkg/errors"
"go.opentelemetry.io/collector/pdata/pcommon"
"go.opentelemetry.io/collector/pdata/plog"
"go.opentelemetry.io/collector/processor"
"go.signoz.io/signoz/pkg/query-service/collectorsimulator"
"go.signoz.io/signoz/pkg/query-service/model"
)
@ -66,14 +66,20 @@ func SimulatePipelinesProcessing(
return updatedConf, nil
}
outputPLogs, collectorErrs, apiErr := collectorsimulator.SimulateLogsProcessing(
outputPLogs, collectorErrs, simulationErr := collectorsimulator.SimulateLogsProcessing(
ctx,
processorFactories,
configGenerator,
simulatorInputPLogs,
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,
"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
for queryName, query := range compositeQuery.BuilderQueries {
if query.Expression != queryName && query.DataSource != v3.DataSourceLogs {
if query.Expression != queryName || query.DataSource != v3.DataSourceLogs {
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) {
var postData *model.GetServicesParams
@ -289,229 +267,6 @@ func DoesExistInSlice(item string, list []string) bool {
}
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) {
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"
metricsV3 "go.signoz.io/signoz/pkg/query-service/app/metrics/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/constants"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
@ -158,11 +159,16 @@ func (q *querier) runBuilderQuery(
if builderQuery.DataSource == v3.DataSourceTraces {
tracesQueryBuilder := tracesV3.PrepareTracesQuery
if q.UseTraceNewSchema {
tracesQueryBuilder = tracesV4.PrepareTracesQuery
}
var query string
var err error
// for ts query with group by and limit form two queries
if params.CompositeQuery.PanelType == v3.PanelTypeGraph && builderQuery.Limit > 0 && len(builderQuery.GroupBy) > 0 {
limitQuery, err := tracesV3.PrepareTracesQuery(
limitQuery, err := tracesQueryBuilder(
start,
end,
params.CompositeQuery.PanelType,
@ -173,7 +179,7 @@ func (q *querier) runBuilderQuery(
ch <- channelResult{Err: err, Name: queryName, Query: limitQuery, Series: nil}
return
}
placeholderQuery, err := tracesV3.PrepareTracesQuery(
placeholderQuery, err := tracesQueryBuilder(
start,
end,
params.CompositeQuery.PanelType,
@ -186,7 +192,7 @@ func (q *querier) runBuilderQuery(
}
query = fmt.Sprintf(placeholderQuery, limitQuery)
} else {
query, err = tracesV3.PrepareTracesQuery(
query, err = tracesQueryBuilder(
start,
end,
params.CompositeQuery.PanelType,

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