Compare commits

..

1 Commits

Author SHA1 Message Date
nikhilmantri0902
1c7b37d3e5 chore: initial commit 2026-03-31 11:33:39 +05:30
273 changed files with 5125 additions and 16521 deletions

View File

@@ -27,8 +27,8 @@ services:
- ${PWD}/fs/tmp/var/lib/clickhouse/user_scripts/:/var/lib/clickhouse/user_scripts/
- ${PWD}/../../../deploy/common/clickhouse/custom-function.xml:/etc/clickhouse-server/custom-function.xml
ports:
- "127.0.0.1:8123:8123"
- "127.0.0.1:9000:9000"
- '127.0.0.1:8123:8123'
- '127.0.0.1:9000:9000'
tty: true
healthcheck:
test:
@@ -47,16 +47,13 @@ services:
condition: service_healthy
environment:
- CLICKHOUSE_SKIP_USER_SETUP=1
networks:
- default
- signoz-devenv
zookeeper:
image: signoz/zookeeper:3.7.1
container_name: zookeeper
volumes:
- ${PWD}/fs/tmp/zookeeper:/bitnami/zookeeper
ports:
- "127.0.0.1:2181:2181"
- '127.0.0.1:2181:2181'
environment:
- ALLOW_ANONYMOUS_LOGIN=yes
healthcheck:
@@ -77,19 +74,12 @@ services:
entrypoint:
- /bin/sh
command:
- -c
- |
/signoz-otel-collector migrate bootstrap &&
/signoz-otel-collector migrate sync up &&
/signoz-otel-collector migrate async up
- -c
- |
/signoz-otel-collector migrate bootstrap &&
/signoz-otel-collector migrate sync up &&
/signoz-otel-collector migrate async up
depends_on:
clickhouse:
condition: service_healthy
restart: on-failure
networks:
- default
- signoz-devenv
networks:
signoz-devenv:
name: signoz-devenv

View File

@@ -3,7 +3,7 @@ services:
image: signoz/signoz-otel-collector:v0.142.0
container_name: signoz-otel-collector-dev
entrypoint:
- /bin/sh
- /bin/sh
command:
- -c
- |
@@ -34,11 +34,4 @@ services:
retries: 3
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- default
- signoz-devenv
networks:
signoz-devenv:
name: signoz-devenv
- "host.docker.internal:host-gateway"

View File

@@ -12,10 +12,10 @@ receivers:
scrape_configs:
- job_name: otel-collector
static_configs:
- targets:
- localhost:8888
labels:
job_name: otel-collector
- targets:
- localhost:8888
labels:
job_name: otel-collector
processors:
batch:
@@ -29,26 +29,7 @@ processors:
signozspanmetrics/delta:
metrics_exporter: signozclickhousemetrics
metrics_flush_interval: 60s
latency_histogram_buckets:
[
100us,
1ms,
2ms,
6ms,
10ms,
50ms,
100ms,
250ms,
500ms,
1000ms,
1400ms,
2000ms,
5s,
10s,
20s,
40s,
60s,
]
latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ]
dimensions_cache_size: 100000
aggregation_temporality: AGGREGATION_TEMPORALITY_DELTA
enable_exp_histogram: true
@@ -79,13 +60,13 @@ extensions:
exporters:
clickhousetraces:
datasource: tcp://clickhouse:9000/signoz_traces
datasource: tcp://host.docker.internal:9000/signoz_traces
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
use_new_schema: true
signozclickhousemetrics:
dsn: tcp://clickhouse:9000/signoz_metrics
dsn: tcp://host.docker.internal:9000/signoz_metrics
clickhouselogsexporter:
dsn: tcp://clickhouse:9000/signoz_logs
dsn: tcp://host.docker.internal:9000/signoz_logs
timeout: 10s
use_new_schema: true
@@ -112,4 +93,4 @@ service:
logs:
receivers: [otlp]
processors: [batch]
exporters: [clickhouselogsexporter]
exporters: [clickhouselogsexporter]

View File

@@ -6,14 +6,12 @@ linters:
- depguard
- errcheck
- forbidigo
- godot
- govet
- iface
- ineffassign
- misspell
- nilnil
- sloglint
- staticcheck
- wastedassign
- unparam
- unused

View File

@@ -85,12 +85,10 @@ sqlstore:
sqlite:
# The path to the SQLite database file.
path: /var/lib/signoz/signoz.db
# The journal mode for the sqlite database. Supported values: delete, wal.
# Mode is the mode to use for the sqlite database.
mode: delete
# The timeout for the sqlite database to wait for a lock.
# BusyTimeout is the timeout for the sqlite database to wait for a lock.
busy_timeout: 10s
# The default transaction locking behavior. Supported values: deferred, immediate, exclusive.
transaction_mode: deferred
##################### APIServer #####################
apiserver:

View File

@@ -1114,33 +1114,6 @@ components:
enabled:
type: boolean
type: object
MetricsexplorertypesInspectMetricsRequest:
properties:
end:
format: int64
type: integer
filter:
$ref: '#/components/schemas/Querybuildertypesv5Filter'
metricName:
type: string
start:
format: int64
type: integer
required:
- metricName
- start
- end
type: object
MetricsexplorertypesInspectMetricsResponse:
properties:
series:
items:
$ref: '#/components/schemas/Querybuildertypesv5TimeSeries'
nullable: true
type: array
required:
- series
type: object
MetricsexplorertypesListMetric:
properties:
description:
@@ -1289,13 +1262,6 @@ components:
- temporality
- isMonotonic
type: object
MetricsexplorertypesMetricsOnboardingResponse:
properties:
hasMetrics:
type: boolean
required:
- hasMetrics
type: object
MetricsexplorertypesStat:
properties:
description:
@@ -7784,111 +7750,6 @@ paths:
summary: Update metric metadata
tags:
- metrics
/api/v2/metrics/inspect:
post:
deprecated: false
description: Returns raw time series data points for a metric within a time
range (max 30 minutes). Each series includes labels and timestamp/value pairs.
operationId: InspectMetrics
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/MetricsexplorertypesInspectMetricsRequest'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/MetricsexplorertypesInspectMetricsResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Inspect raw metric data points
tags:
- metrics
/api/v2/metrics/onboarding:
get:
deprecated: false
description: Lightweight endpoint that checks if any non-SigNoz metrics have
been ingested, used for onboarding status detection
operationId: GetMetricsOnboardingStatus
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/MetricsexplorertypesMetricsOnboardingResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Check if non-SigNoz metrics have been received
tags:
- metrics
/api/v2/metrics/stats:
post:
deprecated: false

View File

@@ -16,7 +16,7 @@ func (hp *HourlyProvider) GetBaseSeasonalProvider() *BaseSeasonalProvider {
return &hp.BaseSeasonalProvider
}
// NewHourlyProvider now uses the generic option type.
// NewHourlyProvider now uses the generic option type
func NewHourlyProvider(opts ...GenericProviderOption[*HourlyProvider]) *HourlyProvider {
hp := &HourlyProvider{
BaseSeasonalProvider: BaseSeasonalProvider{},

View File

@@ -47,7 +47,7 @@ type AnomaliesResponse struct {
// | |
// (rounded value for past peiod) + (seasonal growth)
//
// score = abs(value - prediction) / stddev (current_season_query).
// score = abs(value - prediction) / stddev (current_season_query)
type anomalyQueryParams struct {
// CurrentPeriodQuery is the query range params for period user is looking at or eval window
// Example: (now-5m, now), (now-30m, now), (now-1h, now)

View File

@@ -18,12 +18,12 @@ var (
movingAvgWindowSize = 7
)
// BaseProvider is an interface that includes common methods for all provider types.
// BaseProvider is an interface that includes common methods for all provider types
type BaseProvider interface {
GetBaseSeasonalProvider() *BaseSeasonalProvider
}
// GenericProviderOption is a generic type for provider options.
// GenericProviderOption is a generic type for provider options
type GenericProviderOption[T BaseProvider] func(T)
func WithQuerier[T BaseProvider](querier querier.Querier) GenericProviderOption[T] {
@@ -121,7 +121,7 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, orgID valuer.UUID
}
// getMatchingSeries gets the matching series from the query result
// for the given series.
// for the given series
func (p *BaseSeasonalProvider) getMatchingSeries(_ context.Context, queryResult *qbtypes.TimeSeriesData, series *qbtypes.TimeSeries) *qbtypes.TimeSeries {
if queryResult == nil || len(queryResult.Aggregations) == 0 || len(queryResult.Aggregations[0].Series) == 0 {
return nil
@@ -155,14 +155,13 @@ func (p *BaseSeasonalProvider) getStdDev(series *qbtypes.TimeSeries) float64 {
avg := p.getAvg(series)
var sum float64
for _, smpl := range series.Values {
d := smpl.Value - avg
sum += d * d
sum += math.Pow(smpl.Value-avg, 2)
}
return math.Sqrt(sum / float64(len(series.Values)))
}
// getMovingAvg gets the moving average for the given series
// for the given window size and start index.
// for the given window size and start index
func (p *BaseSeasonalProvider) getMovingAvg(series *qbtypes.TimeSeries, movingAvgWindowSize, startIdx int) float64 {
if series == nil || len(series.Values) == 0 {
return 0
@@ -237,7 +236,7 @@ func (p *BaseSeasonalProvider) getPredictedSeries(
// getBounds gets the upper and lower bounds for the given series
// for the given z score threshold
// moving avg of the previous period series + z score threshold * std dev of the series
// moving avg of the previous period series - z score threshold * std dev of the series.
// moving avg of the previous period series - z score threshold * std dev of the series
func (p *BaseSeasonalProvider) getBounds(
series, predictedSeries, weekSeries *qbtypes.TimeSeries,
zScoreThreshold float64,
@@ -270,7 +269,7 @@ func (p *BaseSeasonalProvider) getBounds(
// getExpectedValue gets the expected value for the given series
// for the given index
// prevSeriesAvg + currentSeasonSeriesAvg - mean of past season series, past2 season series and past3 season series.
// prevSeriesAvg + currentSeasonSeriesAvg - mean of past season series, past2 season series and past3 season series
func (p *BaseSeasonalProvider) getExpectedValue(
_, prevSeries, currentSeasonSeries, pastSeasonSeries, past2SeasonSeries, past3SeasonSeries *qbtypes.TimeSeries, idx int,
) float64 {
@@ -284,7 +283,7 @@ func (p *BaseSeasonalProvider) getExpectedValue(
// getScore gets the anomaly score for the given series
// for the given index
// (value - expectedValue) / std dev of the series.
// (value - expectedValue) / std dev of the series
func (p *BaseSeasonalProvider) getScore(
series, prevSeries, weekSeries, weekPrevSeries, past2SeasonSeries, past3SeasonSeries *qbtypes.TimeSeries, value float64, idx int,
) float64 {
@@ -297,7 +296,7 @@ func (p *BaseSeasonalProvider) getScore(
// getAnomalyScores gets the anomaly scores for the given series
// for the given index
// (value - expectedValue) / std dev of the series.
// (value - expectedValue) / std dev of the series
func (p *BaseSeasonalProvider) getAnomalyScores(
series, prevSeries, currentSeasonSeries, pastSeasonSeries, past2SeasonSeries, past3SeasonSeries *qbtypes.TimeSeries,
) *qbtypes.TimeSeries {

View File

@@ -1,143 +0,0 @@
package otlphttpauditor
import (
"bytes"
"context"
"io"
"log/slog"
"net/http"
"github.com/SigNoz/signoz/pkg/auditor"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/audittypes"
collogspb "go.opentelemetry.io/proto/otlp/collector/logs/v1"
"google.golang.org/protobuf/proto"
spb "google.golang.org/genproto/googleapis/rpc/status"
)
const (
maxHTTPResponseReadBytes int64 = 64 * 1024
protobufContentType string = "application/x-protobuf"
)
func (provider *provider) export(ctx context.Context, events []audittypes.AuditEvent) error {
logs := audittypes.NewPLogsFromAuditEvents(events, "signoz", provider.build.Version(), "signoz.audit")
request, err := provider.marshaler.MarshalLogs(logs)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, auditor.ErrCodeAuditExportFailed, "failed to marshal audit logs")
}
if err := provider.send(ctx, request); err != nil {
provider.settings.Logger().ErrorContext(ctx, "audit export failed", errors.Attr(err), slog.Int("dropped_log_records", len(events)))
return err
}
return nil
}
// Posts a protobuf-encoded OTLP request to the configured endpoint.
// Retries are handled by the underlying heimdall HTTP client.
// Ref: https://github.com/open-telemetry/opentelemetry-collector/blob/main/exporter/otlphttpexporter/otlp.go
func (provider *provider) send(ctx context.Context, body []byte) error {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, provider.config.OTLPHTTP.Endpoint.String(), bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", protobufContentType)
res, err := provider.httpClient.Do(req)
if err != nil {
return err
}
defer func() {
_, _ = io.CopyN(io.Discard, res.Body, maxHTTPResponseReadBytes)
res.Body.Close()
}()
if res.StatusCode >= 200 && res.StatusCode <= 299 {
provider.onSuccess(ctx, res)
return nil
}
return provider.onErr(res)
}
// Ref: https://github.com/open-telemetry/opentelemetry-collector/blob/01b07fcbb7a253bd996c290dcae6166e71d13732/exporter/otlphttpexporter/otlp.go#L403.
func (provider *provider) onSuccess(ctx context.Context, res *http.Response) {
resBytes, err := readResponseBody(res)
if err != nil || resBytes == nil {
return
}
exportResponse := &collogspb.ExportLogsServiceResponse{}
if err := proto.Unmarshal(resBytes, exportResponse); err != nil {
return
}
ps := exportResponse.GetPartialSuccess()
if ps == nil {
return
}
if ps.GetErrorMessage() != "" || ps.GetRejectedLogRecords() != 0 {
provider.settings.Logger().WarnContext(ctx, "partial success response", slog.String("message", ps.GetErrorMessage()), slog.Int64("dropped_log_records", ps.GetRejectedLogRecords()))
}
}
func (provider *provider) onErr(res *http.Response) error {
status := readResponseStatus(res)
if status != nil {
return errors.Newf(errors.TypeInternal, auditor.ErrCodeAuditExportFailed, "request to %s responded with status code %d, Message=%s, Details=%v", provider.config.OTLPHTTP.Endpoint.String(), res.StatusCode, status.Message, status.Details)
}
return errors.Newf(errors.TypeInternal, auditor.ErrCodeAuditExportFailed, "request to %s responded with status code %d", provider.config.OTLPHTTP.Endpoint.String(), res.StatusCode)
}
// Reads at most maxHTTPResponseReadBytes from the response body.
// Ref: https://github.com/open-telemetry/opentelemetry-collector/blob/01b07fcbb7a253bd996c290dcae6166e71d13732/exporter/otlphttpexporter/otlp.go#L275.
func readResponseBody(resp *http.Response) ([]byte, error) {
if resp.ContentLength == 0 {
return nil, nil
}
maxRead := resp.ContentLength
if maxRead == -1 || maxRead > maxHTTPResponseReadBytes {
maxRead = maxHTTPResponseReadBytes
}
protoBytes := make([]byte, maxRead)
n, err := io.ReadFull(resp.Body, protoBytes)
if n == 0 && (err == nil || errors.Is(err, io.EOF)) {
return nil, nil
}
if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) {
return nil, err
}
return protoBytes[:n], nil
}
// Decodes a protobuf-encoded Status from 4xx/5xx response bodies. Returns nil if the response is empty or cannot be decoded.
// Ref: https://github.com/open-telemetry/opentelemetry-collector/blob/01b07fcbb7a253bd996c290dcae6166e71d13732/exporter/otlphttpexporter/otlp.go#L310.
func readResponseStatus(resp *http.Response) *spb.Status {
if resp.StatusCode < 400 || resp.StatusCode > 599 {
return nil
}
respBytes, err := readResponseBody(resp)
if err != nil || respBytes == nil {
return nil
}
respStatus := &spb.Status{}
if err := proto.Unmarshal(respBytes, respStatus); err != nil {
return nil
}
return respStatus
}

View File

@@ -1,97 +0,0 @@
package otlphttpauditor
import (
"context"
"github.com/SigNoz/signoz/pkg/auditor"
"github.com/SigNoz/signoz/pkg/auditor/auditorserver"
"github.com/SigNoz/signoz/pkg/factory"
client "github.com/SigNoz/signoz/pkg/http/client"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/types/audittypes"
"github.com/SigNoz/signoz/pkg/version"
"go.opentelemetry.io/collector/pdata/plog"
)
var _ auditor.Auditor = (*provider)(nil)
type provider struct {
settings factory.ScopedProviderSettings
config auditor.Config
licensing licensing.Licensing
build version.Build
server *auditorserver.Server
marshaler plog.ProtoMarshaler
httpClient *client.Client
}
func NewFactory(licensing licensing.Licensing, build version.Build) factory.ProviderFactory[auditor.Auditor, auditor.Config] {
return factory.NewProviderFactory(factory.MustNewName("otlphttp"), func(ctx context.Context, providerSettings factory.ProviderSettings, config auditor.Config) (auditor.Auditor, error) {
return newProvider(ctx, providerSettings, config, licensing, build)
})
}
func newProvider(_ context.Context, providerSettings factory.ProviderSettings, config auditor.Config, licensing licensing.Licensing, build version.Build) (auditor.Auditor, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/ee/auditor/otlphttpauditor")
httpClient, err := client.New(
settings.Logger(),
providerSettings.TracerProvider,
providerSettings.MeterProvider,
client.WithTimeout(config.OTLPHTTP.Timeout),
client.WithRetryCount(retryCountFromConfig(config.OTLPHTTP.Retry)),
retrierOption(config.OTLPHTTP.Retry),
)
if err != nil {
return nil, err
}
provider := &provider{
settings: settings,
config: config,
licensing: licensing,
build: build,
marshaler: plog.ProtoMarshaler{},
httpClient: httpClient,
}
server, err := auditorserver.New(settings,
auditorserver.Config{
BufferSize: config.BufferSize,
BatchSize: config.BatchSize,
FlushInterval: config.FlushInterval,
},
provider.export,
)
if err != nil {
return nil, err
}
provider.server = server
return provider, nil
}
func (provider *provider) Start(ctx context.Context) error {
return provider.server.Start(ctx)
}
func (provider *provider) Audit(ctx context.Context, event audittypes.AuditEvent) {
if event.PrincipalOrgID.IsZero() {
provider.settings.Logger().WarnContext(ctx, "audit event dropped as org_id is zero")
return
}
if _, err := provider.licensing.GetActive(ctx, event.PrincipalOrgID); err != nil {
return
}
provider.server.Add(ctx, event)
}
func (provider *provider) Healthy() <-chan struct{} {
return provider.server.Healthy()
}
func (provider *provider) Stop(ctx context.Context) error {
return provider.server.Stop(ctx)
}

View File

@@ -1,52 +0,0 @@
package otlphttpauditor
import (
"time"
"github.com/SigNoz/signoz/pkg/auditor"
client "github.com/SigNoz/signoz/pkg/http/client"
)
// retrier implements client.Retriable with exponential backoff
// derived from auditor.RetryConfig.
type retrier struct {
initialInterval time.Duration
maxInterval time.Duration
}
func newRetrier(cfg auditor.RetryConfig) *retrier {
return &retrier{
initialInterval: cfg.InitialInterval,
maxInterval: cfg.MaxInterval,
}
}
// NextInterval returns the backoff duration for the given retry attempt.
// Uses exponential backoff: initialInterval * 2^retry, capped at maxInterval.
func (r *retrier) NextInterval(retry int) time.Duration {
interval := r.initialInterval
for range retry {
interval *= 2
}
return min(interval, r.maxInterval)
}
func retrierOption(cfg auditor.RetryConfig) client.Option {
return client.WithRetriable(newRetrier(cfg))
}
func retryCountFromConfig(cfg auditor.RetryConfig) int {
if !cfg.Enabled || cfg.MaxElapsedTime <= 0 {
return 0
}
count := 0
elapsed := time.Duration(0)
interval := cfg.InitialInterval
for elapsed < cfg.MaxElapsedTime {
elapsed += interval
interval = min(interval*2, cfg.MaxInterval)
count++
}
return count
}

View File

@@ -13,7 +13,7 @@ var (
once sync.Once
)
// Config initializes the licensing configuration.
// initializes the licensing configuration
func Config(pollInterval time.Duration, failureThreshold int) licensing.Config {
once.Do(func() {
config = licensing.Config{PollInterval: pollInterval, FailureThreshold: failureThreshold}

View File

@@ -79,7 +79,7 @@ func (h *handler) QueryRange(rw http.ResponseWriter, req *http.Request) {
// Build step intervals from the anomaly query
stepIntervals := make(map[string]uint64)
if anomalyQuery.StepInterval.Duration > 0 {
stepIntervals[anomalyQuery.Name] = uint64(anomalyQuery.StepInterval.Seconds())
stepIntervals[anomalyQuery.Name] = uint64(anomalyQuery.StepInterval.Duration.Seconds())
}
finalResp := &qbtypes.QueryRangeResponse{

View File

@@ -242,6 +242,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
apiHandler.RegisterWebSocketPaths(r, am)
apiHandler.RegisterMessagingQueuesRoutes(r, am)
apiHandler.RegisterThirdPartyApiRoutes(r, am)
apiHandler.MetricExplorerRoutes(r, am)
apiHandler.RegisterTraceFunnelsRoutes(r, am)
err := s.signoz.APIServer.AddToRouter(r)

View File

@@ -336,10 +336,9 @@ func (dialect *dialect) UpdatePrimaryKey(ctx context.Context, bun bun.IDB, oldMo
}
fkReference := ""
switch reference {
case Org:
if reference == Org {
fkReference = OrgReference
case User:
} else if reference == User {
fkReference = UserReference
}
@@ -393,10 +392,9 @@ func (dialect *dialect) AddPrimaryKey(ctx context.Context, bun bun.IDB, oldModel
}
fkReference := ""
switch reference {
case Org:
if reference == Org {
fkReference = OrgReference
case User:
} else if reference == User {
fkReference = UserReference
}

View File

@@ -19,7 +19,7 @@ var (
once sync.Once
)
// initializes the Zeus configuration.
// initializes the Zeus configuration
func Config() zeus.Config {
once.Do(func() {
parsedURL, err := neturl.Parse(url)

View File

@@ -189,7 +189,7 @@ func (provider *Provider) do(ctx context.Context, url *url.URL, method string, k
return nil, provider.errFromStatusCode(response.StatusCode, errorMessage)
}
// This can be taken down to the client package.
// This can be taken down to the client package
func (provider *Provider) errFromStatusCode(statusCode int, errorMessage string) error {
switch statusCode {
case http.StatusBadRequest:

View File

@@ -12,7 +12,7 @@
or
`docker build . -t tagname`
**Tag to remote url- Introduce versioning later on**
**Tag to remote url- Introduce versinoing later on**
```
docker tag signoz/frontend:latest 7296823551/signoz:latest

View File

@@ -101,22 +101,6 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
preference.name === ORG_PREFERENCES.ORG_ONBOARDING,
)?.value;
// Don't redirect to onboarding if workspace has issues (blocked, suspended, or restricted)
// User needs access to settings/billing to fix payment issues
const isWorkspaceBlocked = trialInfo?.workSpaceBlock;
const isWorkspaceSuspended = activeLicense?.state === LicenseState.DEFAULTED;
const isWorkspaceAccessRestricted =
activeLicense?.state === LicenseState.TERMINATED ||
activeLicense?.state === LicenseState.EXPIRED ||
activeLicense?.state === LicenseState.CANCELLED;
const hasWorkspaceIssue =
isWorkspaceBlocked || isWorkspaceSuspended || isWorkspaceAccessRestricted;
if (hasWorkspaceIssue) {
return;
}
const isFirstUser = checkFirstTimeUser();
if (
isFirstUser &&
@@ -135,36 +119,40 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
orgPreferences,
usersData,
pathname,
trialInfo?.workSpaceBlock,
activeLicense?.state,
]);
const navigateToWorkSpaceBlocked = useCallback((): void => {
const navigateToWorkSpaceBlocked = (route: any): void => {
const { path } = route;
const isRouteEnabledForWorkspaceBlockedState =
isAdmin &&
(pathname === ROUTES.SETTINGS ||
pathname === ROUTES.ORG_SETTINGS ||
pathname === ROUTES.MEMBERS_SETTINGS ||
pathname === ROUTES.BILLING ||
pathname === ROUTES.MY_SETTINGS);
(path === ROUTES.SETTINGS ||
path === ROUTES.ORG_SETTINGS ||
path === ROUTES.MEMBERS_SETTINGS ||
path === ROUTES.BILLING ||
path === ROUTES.MY_SETTINGS);
if (
pathname &&
pathname !== ROUTES.WORKSPACE_LOCKED &&
path &&
path !== ROUTES.WORKSPACE_LOCKED &&
!isRouteEnabledForWorkspaceBlockedState
) {
history.push(ROUTES.WORKSPACE_LOCKED);
}
}, [isAdmin, pathname]);
};
const navigateToWorkSpaceAccessRestricted = useCallback((): void => {
if (pathname && pathname !== ROUTES.WORKSPACE_ACCESS_RESTRICTED) {
const navigateToWorkSpaceAccessRestricted = (route: any): void => {
const { path } = route;
if (path && path !== ROUTES.WORKSPACE_ACCESS_RESTRICTED) {
history.push(ROUTES.WORKSPACE_ACCESS_RESTRICTED);
}
}, [pathname]);
};
useEffect(() => {
if (!isFetchingActiveLicense && activeLicense) {
const currentRoute = mapRoutes.get('current');
const isTerminated = activeLicense.state === LicenseState.TERMINATED;
const isExpired = activeLicense.state === LicenseState.EXPIRED;
const isCancelled = activeLicense.state === LicenseState.CANCELLED;
@@ -173,53 +161,61 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
const { platform } = activeLicense;
if (isWorkspaceAccessRestricted && platform === LicensePlatform.CLOUD) {
navigateToWorkSpaceAccessRestricted();
if (
isWorkspaceAccessRestricted &&
platform === LicensePlatform.CLOUD &&
currentRoute
) {
navigateToWorkSpaceAccessRestricted(currentRoute);
}
}
}, [
isFetchingActiveLicense,
activeLicense,
navigateToWorkSpaceAccessRestricted,
]);
}, [isFetchingActiveLicense, activeLicense, mapRoutes, pathname]);
useEffect(() => {
if (!isFetchingActiveLicense) {
const currentRoute = mapRoutes.get('current');
const shouldBlockWorkspace = trialInfo?.workSpaceBlock;
if (
shouldBlockWorkspace &&
currentRoute &&
activeLicense?.platform === LicensePlatform.CLOUD
) {
navigateToWorkSpaceBlocked();
navigateToWorkSpaceBlocked(currentRoute);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
isFetchingActiveLicense,
trialInfo?.workSpaceBlock,
activeLicense?.platform,
navigateToWorkSpaceBlocked,
mapRoutes,
pathname,
]);
const navigateToWorkSpaceSuspended = useCallback((): void => {
if (pathname && pathname !== ROUTES.WORKSPACE_SUSPENDED) {
const navigateToWorkSpaceSuspended = (route: any): void => {
const { path } = route;
if (path && path !== ROUTES.WORKSPACE_SUSPENDED) {
history.push(ROUTES.WORKSPACE_SUSPENDED);
}
}, [pathname]);
};
useEffect(() => {
if (!isFetchingActiveLicense && activeLicense) {
const currentRoute = mapRoutes.get('current');
const shouldSuspendWorkspace =
activeLicense.state === LicenseState.DEFAULTED;
if (
shouldSuspendWorkspace &&
currentRoute &&
activeLicense.platform === LicensePlatform.CLOUD
) {
navigateToWorkSpaceSuspended();
navigateToWorkSpaceSuspended(currentRoute);
}
}
}, [isFetchingActiveLicense, activeLicense, navigateToWorkSpaceSuspended]);
}, [isFetchingActiveLicense, activeLicense, mapRoutes, pathname]);
useEffect(() => {
if (org && org.length > 0 && org[0].id !== undefined) {

File diff suppressed because it is too large Load Diff

View File

@@ -31,13 +31,10 @@ import type {
GetMetricHighlightsPathParameters,
GetMetricMetadata200,
GetMetricMetadataPathParameters,
GetMetricsOnboardingStatus200,
GetMetricsStats200,
GetMetricsTreemap200,
InspectMetrics200,
ListMetrics200,
ListMetricsParams,
MetricsexplorertypesInspectMetricsRequestDTO,
MetricsexplorertypesStatsRequestDTO,
MetricsexplorertypesTreemapRequestDTO,
MetricsexplorertypesUpdateMetricMetadataRequestDTO,
@@ -781,176 +778,6 @@ export const useUpdateMetricMetadata = <
return useMutation(mutationOptions);
};
/**
* Returns raw time series data points for a metric within a time range (max 30 minutes). Each series includes labels and timestamp/value pairs.
* @summary Inspect raw metric data points
*/
export const inspectMetrics = (
metricsexplorertypesInspectMetricsRequestDTO: BodyType<MetricsexplorertypesInspectMetricsRequestDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<InspectMetrics200>({
url: `/api/v2/metrics/inspect`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: metricsexplorertypesInspectMetricsRequestDTO,
signal,
});
};
export const getInspectMetricsMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof inspectMetrics>>,
TError,
{ data: BodyType<MetricsexplorertypesInspectMetricsRequestDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof inspectMetrics>>,
TError,
{ data: BodyType<MetricsexplorertypesInspectMetricsRequestDTO> },
TContext
> => {
const mutationKey = ['inspectMetrics'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof inspectMetrics>>,
{ data: BodyType<MetricsexplorertypesInspectMetricsRequestDTO> }
> = (props) => {
const { data } = props ?? {};
return inspectMetrics(data);
};
return { mutationFn, ...mutationOptions };
};
export type InspectMetricsMutationResult = NonNullable<
Awaited<ReturnType<typeof inspectMetrics>>
>;
export type InspectMetricsMutationBody = BodyType<MetricsexplorertypesInspectMetricsRequestDTO>;
export type InspectMetricsMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Inspect raw metric data points
*/
export const useInspectMetrics = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof inspectMetrics>>,
TError,
{ data: BodyType<MetricsexplorertypesInspectMetricsRequestDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof inspectMetrics>>,
TError,
{ data: BodyType<MetricsexplorertypesInspectMetricsRequestDTO> },
TContext
> => {
const mutationOptions = getInspectMetricsMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* Lightweight endpoint that checks if any non-SigNoz metrics have been ingested, used for onboarding status detection
* @summary Check if non-SigNoz metrics have been received
*/
export const getMetricsOnboardingStatus = (signal?: AbortSignal) => {
return GeneratedAPIInstance<GetMetricsOnboardingStatus200>({
url: `/api/v2/metrics/onboarding`,
method: 'GET',
signal,
});
};
export const getGetMetricsOnboardingStatusQueryKey = () => {
return [`/api/v2/metrics/onboarding`] as const;
};
export const getGetMetricsOnboardingStatusQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricsOnboardingStatus>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricsOnboardingStatus>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetMetricsOnboardingStatusQueryKey();
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getMetricsOnboardingStatus>>
> = ({ signal }) => getMetricsOnboardingStatus(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getMetricsOnboardingStatus>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetMetricsOnboardingStatusQueryResult = NonNullable<
Awaited<ReturnType<typeof getMetricsOnboardingStatus>>
>;
export type GetMetricsOnboardingStatusQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Check if non-SigNoz metrics have been received
*/
export function useGetMetricsOnboardingStatus<
TData = Awaited<ReturnType<typeof getMetricsOnboardingStatus>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricsOnboardingStatus>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetMetricsOnboardingStatusQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Check if non-SigNoz metrics have been received
*/
export const invalidateGetMetricsOnboardingStatus = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetMetricsOnboardingStatusQueryKey() },
options,
);
return queryClient;
};
/**
* This endpoint provides list of metrics with their number of samples and timeseries for the given time range
* @summary Get metrics statistics

View File

@@ -1363,32 +1363,6 @@ export interface GlobaltypesTokenizerConfigDTO {
enabled?: boolean;
}
export interface MetricsexplorertypesInspectMetricsRequestDTO {
/**
* @type integer
* @format int64
*/
end: number;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type string
*/
metricName: string;
/**
* @type integer
* @format int64
*/
start: number;
}
export interface MetricsexplorertypesInspectMetricsResponseDTO {
/**
* @type array
* @nullable true
*/
series: Querybuildertypesv5TimeSeriesDTO[] | null;
}
export interface MetricsexplorertypesListMetricDTO {
/**
* @type string
@@ -1534,13 +1508,6 @@ export interface MetricsexplorertypesMetricMetadataDTO {
unit: string;
}
export interface MetricsexplorertypesMetricsOnboardingResponseDTO {
/**
* @type boolean
*/
hasMetrics: boolean;
}
export interface MetricsexplorertypesStatDTO {
/**
* @type string
@@ -4424,22 +4391,6 @@ export type GetMetricMetadata200 = {
export type UpdateMetricMetadataPathParameters = {
metricName: string;
};
export type InspectMetrics200 = {
data: MetricsexplorertypesInspectMetricsResponseDTO;
/**
* @type string
*/
status: string;
};
export type GetMetricsOnboardingStatus200 = {
data: MetricsexplorertypesMetricsOnboardingResponseDTO;
/**
* @type string
*/
status: string;
};
export type GetMetricsStats200 = {
data: MetricsexplorertypesStatsResponseDTO;
/**

View File

@@ -0,0 +1,54 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
export interface InspectMetricsRequest {
metricName: string;
start: number;
end: number;
filters: TagFilter;
}
export interface InspectMetricsResponse {
status: string;
data: {
series: InspectMetricsSeries[];
};
}
export interface InspectMetricsSeries {
title?: string;
strokeColor?: string;
labels: Record<string, string>;
labelsArray: Array<Record<string, string>>;
values: InspectMetricsTimestampValue[];
}
interface InspectMetricsTimestampValue {
timestamp: number;
value: string;
}
export const getInspectMetricsDetails = async (
request: InspectMetricsRequest,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<InspectMetricsResponse> | ErrorResponse> => {
try {
const response = await axios.post(`/metrics/inspect`, request, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -0,0 +1,75 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { MetricType } from './getMetricsList';
export interface MetricDetails {
name: string;
description: string;
type: string;
unit: string;
timeseries: number;
samples: number;
timeSeriesTotal: number;
timeSeriesActive: number;
lastReceived: string;
attributes: MetricDetailsAttribute[] | null;
metadata?: {
metric_type: MetricType;
description: string;
unit: string;
temporality?: Temporality;
};
alerts: MetricDetailsAlert[] | null;
dashboards: MetricDetailsDashboard[] | null;
}
export enum Temporality {
CUMULATIVE = 'Cumulative',
DELTA = 'Delta',
}
export interface MetricDetailsAttribute {
key: string;
value: string[];
valueCount: number;
}
export interface MetricDetailsAlert {
alert_name: string;
alert_id: string;
}
export interface MetricDetailsDashboard {
dashboard_name: string;
dashboard_id: string;
}
export interface MetricDetailsResponse {
status: string;
data: MetricDetails;
}
export const getMetricDetails = async (
metricName: string,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<MetricDetailsResponse> | ErrorResponse> => {
try {
const response = await axios.get(`/metrics/${metricName}/metadata`, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -0,0 +1,67 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import {
OrderByPayload,
TreemapViewType,
} from 'container/MetricsExplorer/Summary/types';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
export interface MetricsListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
offset?: number;
limit?: number;
orderBy?: OrderByPayload;
}
export enum MetricType {
SUM = 'Sum',
GAUGE = 'Gauge',
HISTOGRAM = 'Histogram',
SUMMARY = 'Summary',
EXPONENTIAL_HISTOGRAM = 'ExponentialHistogram',
}
export interface MetricsListItemData {
metric_name: string;
description: string;
type: MetricType;
unit: string;
[TreemapViewType.TIMESERIES]: number;
[TreemapViewType.SAMPLES]: number;
lastReceived: string;
}
export interface MetricsListResponse {
status: string;
data: {
metrics: MetricsListItemData[];
total?: number;
};
}
export const getMetricsList = async (
props: MetricsListPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<MetricsListResponse> | ErrorResponse> => {
try {
const response = await axios.post('/metrics', props, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
params: props,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -0,0 +1,44 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
export interface MetricsListFilterKeysResponse {
status: string;
data: {
metricColumns: string[];
attributeKeys: BaseAutocompleteData[];
};
}
export interface GetMetricsListFilterKeysParams {
searchText: string;
limit?: number;
}
export const getMetricsListFilterKeys = async (
params: GetMetricsListFilterKeysParams,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<MetricsListFilterKeysResponse> | ErrorResponse> => {
try {
const response = await axios.get('/metrics/filters/keys', {
params: {
searchText: params.searchText,
limit: params.limit,
},
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -0,0 +1,43 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
export interface MetricsListFilterValuesPayload {
filterAttributeKeyDataType: string;
filterKey: string;
searchText: string;
limit: number;
}
export interface MetricsListFilterValuesResponse {
status: string;
data: {
filterValues: string[];
};
}
export const getMetricsListFilterValues = async (
props: MetricsListFilterValuesPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<
SuccessResponse<MetricsListFilterValuesResponse> | ErrorResponse
> => {
try {
const response = await axios.post('/metrics/filters/values', props, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
params: props,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -0,0 +1,60 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
export interface RelatedMetricsPayload {
start: number;
end: number;
currentMetricName: string;
}
export interface RelatedMetricDashboard {
dashboard_name: string;
dashboard_id: string;
widget_id: string;
widget_name: string;
}
export interface RelatedMetricAlert {
alert_name: string;
alert_id: string;
}
export interface RelatedMetric {
name: string;
query: IBuilderQuery;
dashboards: RelatedMetricDashboard[];
alerts: RelatedMetricAlert[];
}
export interface RelatedMetricsResponse {
status: 'success';
data: {
related_metrics: RelatedMetric[];
};
}
export const getRelatedMetrics = async (
props: RelatedMetricsPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<RelatedMetricsResponse> | ErrorResponse> => {
try {
const response = await axios.post('/metrics/related', props, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
params: props,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,11 +1,10 @@
/* eslint-disable sonarjs/no-duplicate-string */
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useMutation, useQuery } from 'react-query';
import { Color } from '@signozhq/design-tokens';
import { Compass, Dot, House, Plus, Wrench } from '@signozhq/icons';
import { Button, Popover } from 'antd';
import logEvent from 'api/common/logEvent';
import { useGetMetricsOnboardingStatus } from 'api/generated/services/metrics';
import listUserPreferences from 'api/v1/user/preferences/list';
import updateUserPreferenceAPI from 'api/v1/user/preferences/name/update';
import { PersistedAnnouncementBanner } from 'components/AnnouncementBanner';
@@ -16,8 +15,10 @@ import { ORG_PREFERENCES } from 'constants/orgPreferences';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import ROUTES from 'constants/routes';
import { getMetricsListQuery } from 'container/MetricsExplorer/Summary/utils';
import { IS_SERVICE_ACCOUNTS_ENABLED } from 'container/ServiceAccountsSettings/config';
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
import { useGetMetricsList } from 'hooks/metricsExplorer/useGetMetricsList';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
@@ -126,7 +127,38 @@ export default function Home(): JSX.Element {
);
// Detect Metrics
const { data: metricsOnboardingData } = useGetMetricsOnboardingStatus();
const query = useMemo(() => {
const baseQuery = getMetricsListQuery();
let queryStartTime = startTime;
let queryEndTime = endTime;
if (!startTime || !endTime) {
const now = new Date();
const startTime = new Date(now.getTime() - homeInterval);
const endTime = now;
queryStartTime = startTime.getTime();
queryEndTime = endTime.getTime();
}
return {
...baseQuery,
limit: 10,
offset: 0,
filters: {
items: [],
op: 'AND',
},
start: queryStartTime,
end: queryEndTime,
};
}, [startTime, endTime]);
const { data: metricsData } = useGetMetricsList(query, {
enabled: !!query,
queryKey: ['metricsList', query],
});
const [isLogsIngestionActive, setIsLogsIngestionActive] = useState(false);
const [isTracesIngestionActive, setIsTracesIngestionActive] = useState(false);
@@ -252,12 +284,14 @@ export default function Home(): JSX.Element {
}, [tracesData, handleUpdateChecklistDoneItem]);
useEffect(() => {
if (metricsOnboardingData?.data?.hasMetrics) {
const metricsDataTotal = metricsData?.payload?.data?.total ?? 0;
if (metricsDataTotal > 0) {
setIsMetricsIngestionActive(true);
handleUpdateChecklistDoneItem('ADD_DATA_SOURCE');
handleUpdateChecklistDoneItem('SEND_METRICS');
}
}, [metricsOnboardingData, handleUpdateChecklistDoneItem]);
}, [metricsData, handleUpdateChecklistDoneItem]);
useEffect(() => {
logEvent('Homepage: Visited', {});

View File

@@ -48,8 +48,6 @@ import {
formatDataForTable,
getK8sVolumesListColumns,
getK8sVolumesListQuery,
getVolumeListGroupedByRowDataQueryKey,
getVolumesListQueryKey,
K8sVolumesRowData,
} from './utils';
import VolumeDetails from './VolumeDetails';
@@ -169,26 +167,6 @@ function K8sVolumesList({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
const groupedByRowDataQueryKey = useMemo(
() =>
getVolumeListGroupedByRowDataQueryKey(
selectedRowData?.groupedByMeta,
queryFilters,
orderBy,
groupBy,
minTime,
maxTime,
),
[
selectedRowData?.groupedByMeta,
queryFilters,
orderBy,
groupBy,
minTime,
maxTime,
],
);
const {
data: groupedByRowData,
isFetching: isFetchingGroupedByRowData,
@@ -198,7 +176,7 @@ function K8sVolumesList({
} = useGetK8sVolumesList(
fetchGroupedByRowDataQuery as K8sVolumesListPayload,
{
queryKey: groupedByRowDataQueryKey,
queryKey: ['volumeList', fetchGroupedByRowDataQuery],
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
},
undefined,
@@ -243,28 +221,6 @@ function K8sVolumesList({
return queryPayload;
}, [pageSize, currentPage, queryFilters, minTime, maxTime, orderBy, groupBy]);
const volumesListQueryKey = useMemo(() => {
return getVolumesListQueryKey(
selectedVolumeUID,
pageSize,
currentPage,
queryFilters,
orderBy,
groupBy,
minTime,
maxTime,
);
}, [
selectedVolumeUID,
pageSize,
currentPage,
queryFilters,
groupBy,
orderBy,
minTime,
maxTime,
]);
const formattedGroupedByVolumesData = useMemo(
() =>
formatDataForTable(groupedByRowData?.payload?.data?.records || [], groupBy),
@@ -281,7 +237,7 @@ function K8sVolumesList({
const { data, isFetching, isLoading, isError } = useGetK8sVolumesList(
query as K8sVolumesListPayload,
{
queryKey: volumesListQueryKey,
queryKey: ['volumeList', query],
enabled: !!query,
},
undefined,

View File

@@ -77,74 +77,6 @@ export const getK8sVolumesListQuery = (): K8sVolumesListPayload => ({
orderBy: { columnName: 'usage', order: 'desc' },
});
export const getVolumeListGroupedByRowDataQueryKey = (
groupedByMeta: K8sVolumesData['meta'] | undefined,
queryFilters: IBuilderQuery['filters'],
orderBy: { columnName: string; order: 'asc' | 'desc' } | null,
groupBy: IBuilderQuery['groupBy'],
minTime: number,
maxTime: number,
): (string | undefined)[] => {
// When we have grouped by metadata defined
// We need to leave out the min/max time
// Otherwise it will cause a loop
const groupedByMetaStr = JSON.stringify(groupedByMeta || undefined) ?? '';
if (groupedByMetaStr) {
return [
'volumeList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(groupBy),
groupedByMetaStr,
];
}
return [
'volumeList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(groupBy),
groupedByMetaStr,
String(minTime),
String(maxTime),
];
};
export const getVolumesListQueryKey = (
selectedVolumeUID: string | null,
pageSize: number,
currentPage: number,
queryFilters: IBuilderQuery['filters'],
orderBy: { columnName: string; order: 'asc' | 'desc' } | null,
groupBy: IBuilderQuery['groupBy'],
minTime: number,
maxTime: number,
): (string | undefined)[] => {
// When selected volume is defined
// We need to leave out the min/max time
// Otherwise it will cause a loop
if (selectedVolumeUID) {
return [
'volumeList',
String(pageSize),
String(currentPage),
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(groupBy),
];
}
return [
'volumeList',
String(pageSize),
String(currentPage),
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(groupBy),
String(minTime),
String(maxTime),
];
};
const columnsConfig = [
{
title: <div className="column-header-left pvc-name-header">PVC Name</div>,

View File

@@ -1,93 +1,29 @@
import setupCommonMocks from '../commonMocks';
setupCommonMocks();
import { QueryClient, QueryClientProvider } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { MemoryRouter } from 'react-router-dom-v5-compat';
import { MemoryRouter } from 'react-router-dom';
import { render, waitFor } from '@testing-library/react';
import { FeatureKeys } from 'constants/features';
import K8sVolumesList from 'container/InfraMonitoringK8s/Volumes/K8sVolumesList';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { IAppContext, IUser } from 'providers/App/types';
import { QueryBuilderProvider } from 'providers/QueryBuilder';
// eslint-disable-next-line no-restricted-imports
import { applyMiddleware, legacy_createStore as createStore } from 'redux';
import thunk from 'redux-thunk';
import reducers from 'store/reducers';
import { act, render, screen, userEvent, waitFor } from 'tests/test-utils';
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
import { LicenseResModel } from 'types/api/licensesV3/getActive';
import { INFRA_MONITORING_K8S_PARAMS_KEYS } from '../../constants';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
cacheTime: 0,
},
},
});
const SERVER_URL = 'http://localhost/api';
// jsdom does not implement IntersectionObserver — provide a no-op stub
const mockObserver = {
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
};
global.IntersectionObserver = jest
.fn()
.mockImplementation(() => mockObserver) as any;
const mockVolume = {
persistentVolumeClaimName: 'test-pvc',
volumeAvailable: 1000000,
volumeCapacity: 5000000,
volumeInodes: 100,
volumeInodesFree: 50,
volumeInodesUsed: 50,
volumeUsage: 4000000,
meta: {
k8s_cluster_name: 'test-cluster',
k8s_namespace_name: 'test-namespace',
k8s_node_name: 'test-node',
k8s_persistentvolumeclaim_name: 'test-pvc',
k8s_pod_name: 'test-pod',
k8s_pod_uid: 'test-pod-uid',
k8s_statefulset_name: '',
},
};
const mockVolumesResponse = {
status: 'success',
data: {
type: '',
records: [mockVolume],
groups: null,
total: 1,
sentAnyHostMetricsData: false,
isSendingK8SAgentMetrics: false,
},
};
/** Renders K8sVolumesList with a real Redux store so dispatched actions affect state. */
function renderWithRealStore(
initialEntries?: Record<string, any>,
): { testStore: ReturnType<typeof createStore> } {
const testStore = createStore(reducers, applyMiddleware(thunk as any));
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
render(
<NuqsTestingAdapter searchParams={initialEntries}>
<QueryClientProvider client={queryClient}>
<QueryBuilderProvider>
<MemoryRouter>
<K8sVolumesList
isFiltersVisible={false}
handleFilterVisibilityChange={jest.fn()}
quickFiltersLastUpdated={-1}
/>
</MemoryRouter>
</QueryBuilderProvider>
</QueryClientProvider>
</NuqsTestingAdapter>,
);
return { testStore };
}
describe('K8sVolumesList - useGetAggregateKeys Category Regression', () => {
let requestsMade: Array<{
url: string;
@@ -97,6 +33,7 @@ describe('K8sVolumesList - useGetAggregateKeys Category Regression', () => {
beforeEach(() => {
requestsMade = [];
queryClient.clear();
server.use(
rest.get(`${SERVER_URL}/v3/autocomplete/attribute_keys`, (req, res, ctx) => {
@@ -142,7 +79,19 @@ describe('K8sVolumesList - useGetAggregateKeys Category Regression', () => {
});
it('should call aggregate keys API with k8s_volume_capacity', async () => {
renderWithRealStore();
render(
<NuqsTestingAdapter>
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<K8sVolumesList
isFiltersVisible={false}
handleFilterVisibilityChange={jest.fn()}
quickFiltersLastUpdated={-1}
/>
</MemoryRouter>
</QueryClientProvider>
</NuqsTestingAdapter>,
);
await waitFor(() => {
expect(requestsMade.length).toBeGreaterThan(0);
@@ -179,7 +128,19 @@ describe('K8sVolumesList - useGetAggregateKeys Category Regression', () => {
activeLicense: (null as unknown) as LicenseResModel,
} as IAppContext);
renderWithRealStore();
render(
<NuqsTestingAdapter>
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<K8sVolumesList
isFiltersVisible={false}
handleFilterVisibilityChange={jest.fn()}
quickFiltersLastUpdated={-1}
/>
</MemoryRouter>
</QueryClientProvider>
</NuqsTestingAdapter>,
);
await waitFor(() => {
expect(requestsMade.length).toBeGreaterThan(0);
@@ -198,193 +159,3 @@ describe('K8sVolumesList - useGetAggregateKeys Category Regression', () => {
expect(aggregateAttribute).toBe('k8s.volume.capacity');
});
});
describe('K8sVolumesList', () => {
beforeEach(() => {
server.use(
rest.post('http://localhost/api/v1/pvcs/list', (_req, res, ctx) =>
res(ctx.status(200), ctx.json(mockVolumesResponse)),
),
rest.get(
'http://localhost/api/v3/autocomplete/attribute_keys',
(_req, res, ctx) =>
res(ctx.status(200), ctx.json({ data: { attributeKeys: [] } })),
),
);
});
it('renders volume rows from API response', async () => {
renderWithRealStore();
await waitFor(async () => {
const elements = await screen.findAllByText('test-pvc');
expect(elements.length).toBeGreaterThan(0);
});
});
it('opens VolumeDetails when a volume row is clicked', async () => {
const user = userEvent.setup();
renderWithRealStore();
const pvcCells = await screen.findAllByText('test-pvc');
expect(pvcCells.length).toBeGreaterThan(0);
const row = pvcCells[0].closest('tr');
expect(row).not.toBeNull();
await user.click(row!);
await waitFor(async () => {
const cells = await screen.findAllByText('test-pvc');
expect(cells.length).toBeGreaterThan(1);
});
});
it.skip('closes VolumeDetails when the close button is clicked', async () => {
const user = userEvent.setup();
renderWithRealStore();
const pvcCells = await screen.findAllByText('test-pvc');
expect(pvcCells.length).toBeGreaterThan(0);
const row = pvcCells[0].closest('tr');
await user.click(row!);
await waitFor(() => {
expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument();
});
await user.click(screen.getByRole('button', { name: 'Close' }));
await waitFor(() => {
expect(
screen.queryByRole('button', { name: 'Close' }),
).not.toBeInTheDocument();
});
});
it('does not re-fetch the volumes list when time range changes after selecting a volume', async () => {
const user = userEvent.setup();
let apiCallCount = 0;
server.use(
rest.post('http://localhost/api/v1/pvcs/list', async (_req, res, ctx) => {
apiCallCount += 1;
return res(ctx.status(200), ctx.json(mockVolumesResponse));
}),
);
const { testStore } = renderWithRealStore();
await waitFor(() => expect(apiCallCount).toBe(1));
const pvcCells = await screen.findAllByText('test-pvc');
const row = pvcCells[0].closest('tr');
await user.click(row!);
await waitFor(async () => {
const cells = await screen.findAllByText('test-pvc');
expect(cells.length).toBeGreaterThan(1);
});
// Wait for nuqs URL state to fully propagate to the component
// The selectedVolumeUID is managed via nuqs (async URL state),
// so we need to ensure the state has settled before dispatching time changes
await act(async () => {
await new Promise((resolve) => {
setTimeout(resolve, 0);
});
});
const countAfterClick = apiCallCount;
// There's a specific component causing the min/max time to be updated
// After the volume loads, it triggers the change again
// And then the query to fetch data for the selected volume enters in a loop
act(() => {
testStore.dispatch({
type: UPDATE_TIME_INTERVAL,
payload: {
minTime: Date.now() * 1000000 - 30 * 60 * 1000 * 1000000,
maxTime: Date.now() * 1000000,
selectedTime: '30m',
},
} as any);
});
// Allow any potential re-fetch to settle
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
expect(apiCallCount).toBe(countAfterClick);
});
it('does not re-fetch groupedByRowData when time range changes after expanding a volume row with groupBy', async () => {
const user = userEvent.setup();
const groupByValue = [{ key: 'k8s_namespace_name' }];
let groupedByRowDataCallCount = 0;
server.use(
rest.post('http://localhost/api/v1/pvcs/list', async (req, res, ctx) => {
const body = await req.json();
// Check for both underscore and dot notation keys since dotMetricsEnabled
// may be true or false depending on test order
const isGroupedByRowDataRequest = body.filters?.items?.some(
(item: { key?: { key?: string }; value?: string }) =>
(item.key?.key === 'k8s_namespace_name' ||
item.key?.key === 'k8s.namespace.name') &&
item.value === 'test-namespace',
);
if (isGroupedByRowDataRequest) {
groupedByRowDataCallCount += 1;
}
return res(ctx.status(200), ctx.json(mockVolumesResponse));
}),
rest.get(
'http://localhost/api/v3/autocomplete/attribute_keys',
(_req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
data: {
attributeKeys: [{ key: 'k8s_namespace_name', dataType: 'string' }],
},
}),
),
),
);
const { testStore } = renderWithRealStore({
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify(groupByValue),
});
await waitFor(async () => {
const elements = await screen.findAllByText('test-namespace');
return expect(elements.length).toBeGreaterThan(0);
});
const row = (await screen.findAllByText('test-namespace'))[0].closest('tr');
expect(row).not.toBeNull();
user.click(row as HTMLElement);
await waitFor(() => expect(groupedByRowDataCallCount).toBe(1));
const countAfterExpand = groupedByRowDataCallCount;
act(() => {
testStore.dispatch({
type: UPDATE_TIME_INTERVAL,
payload: {
minTime: Date.now() * 1000000 - 30 * 60 * 1000 * 1000000,
maxTime: Date.now() * 1000000,
selectedTime: '30m',
},
} as any);
});
// Allow any potential re-fetch to settle
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
expect(groupedByRowDataCallCount).toBe(countAfterExpand);
});
});

View File

@@ -0,0 +1,37 @@
import { UseQueryResult } from 'react-query';
import { RelatedMetric } from 'api/metricsExplorer/getRelatedMetrics';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
export enum ExplorerTabs {
TIME_SERIES = 'time-series',
RELATED_METRICS = 'related-metrics',
}
export interface TimeSeriesProps {
showOneChartPerQuery: boolean;
}
export interface RelatedMetricsProps {
metricNames: string[];
}
export interface RelatedMetricsCardProps {
metric: RelatedMetricWithQueryResult;
}
export interface UseGetRelatedMetricsGraphsProps {
selectedMetricName: string | null;
startMs: number;
endMs: number;
}
export interface UseGetRelatedMetricsGraphsReturn {
relatedMetrics: RelatedMetricWithQueryResult[];
isRelatedMetricsLoading: boolean;
isRelatedMetricsError: boolean;
}
export interface RelatedMetricWithQueryResult extends RelatedMetric {
queryResult: UseQueryResult<SuccessResponse<MetricRangePayloadProps>, unknown>;
}

View File

@@ -30,6 +30,31 @@
}
}
.explore-tabs {
margin: 15px 0;
.tab {
background-color: var(--bg-slate-500);
border-color: var(--bg-ink-200);
width: 180px;
padding: 16px 0;
display: flex;
justify-content: center;
align-items: center;
}
.tab:first-of-type {
border-top-left-radius: 2px;
}
.tab:last-of-type {
border-top-right-radius: 2px;
}
.selected-view {
background: var(--bg-ink-500);
}
}
.explore-content {
padding: 0 8px;
@@ -91,6 +116,81 @@
width: 100%;
height: fit-content;
}
.related-metrics-container {
width: 100%;
min-height: 300px;
display: flex;
flex-direction: column;
gap: 10px;
.related-metrics-header {
display: flex;
align-items: center;
justify-content: flex-start;
.metric-name-select {
width: 20%;
margin-right: 10px;
}
.related-metrics-input {
width: 40%;
.ant-input-wrapper {
.ant-input-group-addon {
.related-metrics-select {
width: 250px;
border: 1px solid var(--bg-slate-500) !important;
.ant-select-selector {
text-align: left;
color: var(--text-vanilla-500) !important;
}
}
}
}
}
}
.related-metrics-body {
margin-top: 20px;
max-height: 650px;
overflow-y: scroll;
.related-metrics-card-container {
margin-bottom: 20px;
min-height: 640px;
.related-metrics-card {
display: flex;
flex-direction: column;
gap: 16px;
.related-metrics-card-error {
padding-top: 10px;
height: fit-content;
width: fit-content;
}
}
}
}
}
}
}
.lightMode {
.metrics-explorer-explore-container {
.explore-tabs {
.tab {
background-color: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-400);
}
.selected-view {
background: var(--bg-vanilla-500);
}
}
}
}

View File

@@ -32,6 +32,7 @@ import { v4 as uuid } from 'uuid';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
import MetricDetails from '../MetricDetails/MetricDetails';
import TimeSeries from './TimeSeries';
import { ExplorerTabs } from './types';
import {
getMetricUnits,
splitQueryIntoOneChartPerQuery,
@@ -94,6 +95,7 @@ function Explorer(): JSX.Element {
const [disableOneChartPerQuery, toggleDisableOneChartPerQuery] = useState(
false,
);
const [selectedTab] = useState<ExplorerTabs>(ExplorerTabs.TIME_SERIES);
const [yAxisUnit, setYAxisUnit] = useState<string | undefined>();
const unitsLength = useMemo(() => units.length, [units]);
@@ -317,21 +319,48 @@ function Explorer(): JSX.Element {
showFunctions={false}
version="v3"
/>
{/* TODO: Enable once we have resolved all related metrics issues */}
{/* <Button.Group className="explore-tabs">
<Button
value={ExplorerTabs.TIME_SERIES}
className={classNames('tab', {
'selected-view': selectedTab === ExplorerTabs.TIME_SERIES,
})}
onClick={(): void => setSelectedTab(ExplorerTabs.TIME_SERIES)}
>
<Typography.Text>Time series</Typography.Text>
</Button>
<Button
value={ExplorerTabs.RELATED_METRICS}
className={classNames('tab', {
'selected-view': selectedTab === ExplorerTabs.RELATED_METRICS,
})}
onClick={(): void => setSelectedTab(ExplorerTabs.RELATED_METRICS)}
>
<Typography.Text>Related</Typography.Text>
</Button>
</Button.Group> */}
<div className="explore-content">
<TimeSeries
showOneChartPerQuery={showOneChartPerQuery}
setWarning={setWarning}
areAllMetricUnitsSame={areAllMetricUnitsSame}
isMetricUnitsLoading={isMetricUnitsLoading}
isMetricUnitsError={isMetricUnitsError}
metricUnits={units}
metricNames={metricNames}
metrics={metrics}
handleOpenMetricDetails={handleOpenMetricDetails}
yAxisUnit={yAxisUnit}
setYAxisUnit={setYAxisUnit}
showYAxisUnitSelector={showYAxisUnitSelector}
/>
{selectedTab === ExplorerTabs.TIME_SERIES && (
<TimeSeries
showOneChartPerQuery={showOneChartPerQuery}
setWarning={setWarning}
areAllMetricUnitsSame={areAllMetricUnitsSame}
isMetricUnitsLoading={isMetricUnitsLoading}
isMetricUnitsError={isMetricUnitsError}
metricUnits={units}
metricNames={metricNames}
metrics={metrics}
handleOpenMetricDetails={handleOpenMetricDetails}
yAxisUnit={yAxisUnit}
setYAxisUnit={setYAxisUnit}
showYAxisUnitSelector={showYAxisUnitSelector}
/>
)}
{/* TODO: Enable once we have resolved all related metrics issues */}
{/* {selectedTab === ExplorerTabs.RELATED_METRICS && (
<RelatedMetrics metricNames={metricNames} />
)} */}
</div>
</div>
<ExplorerOptionWrapper

View File

@@ -0,0 +1,153 @@
import { useEffect, useMemo, useRef, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { Color } from '@signozhq/design-tokens';
import { Card, Col, Empty, Input, Row, Select, Skeleton } from 'antd';
import { Gauge } from 'lucide-react';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import RelatedMetricsCard from './RelatedMetricsCard';
import { RelatedMetricsProps, RelatedMetricWithQueryResult } from './types';
import { useGetRelatedMetricsGraphs } from './useGetRelatedMetricsGraphs';
function RelatedMetrics({ metricNames }: RelatedMetricsProps): JSX.Element {
const graphRef = useRef<HTMLDivElement>(null);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const [selectedMetricName, setSelectedMetricName] = useState<string | null>(
null,
);
const [selectedRelatedMetric, setSelectedRelatedMetric] = useState('all');
const [searchValue, setSearchValue] = useState<string | null>(null);
const startMs = useMemo(() => Math.floor(Number(minTime) / 1000000000), [
minTime,
]);
const endMs = useMemo(() => Math.floor(Number(maxTime) / 1000000000), [
maxTime,
]);
useEffect(() => {
if (metricNames.length) {
setSelectedMetricName(metricNames[0]);
}
}, [metricNames]);
const {
relatedMetrics,
isRelatedMetricsLoading,
isRelatedMetricsError,
} = useGetRelatedMetricsGraphs({
selectedMetricName,
startMs,
endMs,
});
const metricNamesSelectOptions = useMemo(
() =>
metricNames.map((name) => ({
value: name,
label: name,
})),
[metricNames],
);
const relatedMetricsSelectOptions = useMemo(() => {
const options: { value: string; label: string }[] = [
{
value: 'all',
label: 'All',
},
];
relatedMetrics.forEach((metric) => {
options.push({
value: metric.name,
label: metric.name,
});
});
return options;
}, [relatedMetrics]);
const filteredRelatedMetrics = useMemo(() => {
let filteredMetrics: RelatedMetricWithQueryResult[] = [];
if (selectedRelatedMetric === 'all') {
filteredMetrics = [...relatedMetrics];
} else {
filteredMetrics = relatedMetrics.filter(
(metric) => metric.name === selectedRelatedMetric,
);
}
if (searchValue?.length) {
filteredMetrics = filteredMetrics.filter((metric) =>
metric.name.toLowerCase().includes(searchValue?.toLowerCase() ?? ''),
);
}
return filteredMetrics;
}, [relatedMetrics, selectedRelatedMetric, searchValue]);
return (
<div className="related-metrics-container">
<div className="related-metrics-header">
<Select
className="metric-name-select"
value={selectedMetricName}
options={metricNamesSelectOptions}
onChange={(value): void => setSelectedMetricName(value)}
suffixIcon={<Gauge size={12} color={Color.BG_SAKURA_500} />}
/>
<Input
className="related-metrics-input"
placeholder="Search..."
onChange={(e): void => setSearchValue(e.target.value)}
bordered
addonBefore={
<Select
loading={isRelatedMetricsLoading}
value={selectedRelatedMetric}
className="related-metrics-select"
options={relatedMetricsSelectOptions}
onChange={(value): void => setSelectedRelatedMetric(value)}
bordered={false}
/>
}
/>
</div>
<div className="related-metrics-body">
{isRelatedMetricsLoading && <Skeleton active />}
{isRelatedMetricsError && (
<Empty description="Error fetching related metrics" />
)}
{!isRelatedMetricsLoading &&
!isRelatedMetricsError &&
filteredRelatedMetrics.length === 0 && (
<Empty description="No related metrics found" />
)}
{!isRelatedMetricsLoading &&
!isRelatedMetricsError &&
filteredRelatedMetrics.length > 0 && (
<Row gutter={24}>
{filteredRelatedMetrics.map((relatedMetricWithQueryResult) => (
<Col span={12} key={relatedMetricWithQueryResult.name}>
<Card
bordered
ref={graphRef}
className="related-metrics-card-container"
>
<RelatedMetricsCard
key={relatedMetricWithQueryResult.name}
metric={relatedMetricWithQueryResult}
/>
</Card>
</Col>
))}
</Row>
)}
</div>
</div>
);
}
export default RelatedMetrics;

View File

@@ -0,0 +1,47 @@
import { Empty, Skeleton, Typography } from 'antd';
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
import { DataSource } from 'types/common/queryBuilder';
import DashboardsAndAlertsPopover from '../MetricDetails/DashboardsAndAlertsPopover';
import { RelatedMetricsCardProps } from './types';
function RelatedMetricsCard({ metric }: RelatedMetricsCardProps): JSX.Element {
const { queryResult } = metric;
if (queryResult.isLoading) {
return <Skeleton />;
}
if (queryResult.error) {
const errorMessage =
(queryResult.error as Error)?.message || 'Something went wrong';
return <div>{errorMessage}</div>;
}
return (
<div className="related-metrics-card">
<Typography.Text className="related-metrics-card-name">
{metric.name}
</Typography.Text>
{queryResult.isLoading ? <Skeleton /> : null}
{queryResult.isError ? (
<div className="related-metrics-card-error">
<Empty description="Error fetching metric data" />
</div>
) : null}
{!queryResult.isLoading && !queryResult.error && (
<TimeSeriesView
isFilterApplied={false}
isError={queryResult.isError}
isLoading={queryResult.isLoading}
data={queryResult.data}
yAxisUnit="ms"
dataSource={DataSource.METRICS}
/>
)}
<DashboardsAndAlertsPopover metricName={metric.name} />
</div>
);
}
export default RelatedMetricsCard;

View File

@@ -1,6 +1,14 @@
import { Dispatch, SetStateAction } from 'react';
import { UseQueryResult } from 'react-query';
import { MetricsexplorertypesMetricMetadataDTO } from 'api/generated/services/sigNoz.schemas';
import { Warning } from 'types/api';
import { RelatedMetric } from 'api/metricsExplorer/getRelatedMetrics';
import { SuccessResponse, Warning } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
export enum ExplorerTabs {
TIME_SERIES = 'time-series',
RELATED_METRICS = 'related-metrics',
}
export interface TimeSeriesProps {
showOneChartPerQuery: boolean;
@@ -16,3 +24,27 @@ export interface TimeSeriesProps {
setYAxisUnit: (unit: string) => void;
showYAxisUnitSelector: boolean;
}
export interface RelatedMetricsProps {
metricNames: string[];
}
export interface RelatedMetricsCardProps {
metric: RelatedMetricWithQueryResult;
}
export interface UseGetRelatedMetricsGraphsProps {
selectedMetricName: string | null;
startMs: number;
endMs: number;
}
export interface UseGetRelatedMetricsGraphsReturn {
relatedMetrics: RelatedMetricWithQueryResult[];
isRelatedMetricsLoading: boolean;
isRelatedMetricsError: boolean;
}
export interface RelatedMetricWithQueryResult extends RelatedMetric {
queryResult: UseQueryResult<SuccessResponse<MetricRangePayloadProps>, unknown>;
}

View File

@@ -0,0 +1,113 @@
import { useMemo } from 'react';
import { useQueries } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useGetRelatedMetrics } from 'hooks/metricsExplorer/useGetRelatedMetrics';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { AppState } from 'store/reducers';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as uuidv4 } from 'uuid';
import { convertNanoToMilliseconds } from '../Summary/utils';
import {
UseGetRelatedMetricsGraphsProps,
UseGetRelatedMetricsGraphsReturn,
} from './types';
export const useGetRelatedMetricsGraphs = ({
selectedMetricName,
startMs,
endMs,
}: UseGetRelatedMetricsGraphsProps): UseGetRelatedMetricsGraphsReturn => {
const { maxTime, minTime, selectedTime: globalSelectedTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
// Build the query for the related metrics
const relatedMetricsQuery = useMemo(
() => ({
start: convertNanoToMilliseconds(minTime),
end: convertNanoToMilliseconds(maxTime),
currentMetricName: selectedMetricName ?? '',
}),
[selectedMetricName, minTime, maxTime],
);
// Get the related metrics
const {
data: relatedMetricsData,
isLoading: isRelatedMetricsLoading,
isError: isRelatedMetricsError,
} = useGetRelatedMetrics(relatedMetricsQuery, {
enabled: !!selectedMetricName,
});
// Build the related metrics array
const relatedMetrics = useMemo(() => {
if (relatedMetricsData?.payload?.data?.related_metrics) {
return relatedMetricsData.payload.data.related_metrics;
}
return [];
}, [relatedMetricsData]);
// Build the query results for the related metrics
const relatedMetricsQueryResults = useQueries(
useMemo(
() =>
relatedMetrics.map((metric) => ({
queryKey: ['related-metrics', metric.name],
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
GetMetricQueryRange(
{
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: {
queryData: [metric.query],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [],
id: uuidv4(),
},
graphType: PANEL_TYPES.TIME_SERIES,
selectedTime: 'GLOBAL_TIME',
globalSelectedInterval: globalSelectedTime,
start: startMs,
end: endMs,
formatForWeb: false,
params: {
dataSource: DataSource.METRICS,
},
},
ENTITY_VERSION_V4,
),
enabled: !!metric.query,
})),
[relatedMetrics, globalSelectedTime, startMs, endMs],
),
);
// Build the related metrics with query results
const relatedMetricsWithQueryResults = useMemo(
() =>
relatedMetrics.map((metric, index) => ({
...metric,
queryResult: relatedMetricsQueryResults[index],
})),
[relatedMetrics, relatedMetricsQueryResults],
);
return {
relatedMetrics: relatedMetricsWithQueryResults,
isRelatedMetricsLoading,
isRelatedMetricsError,
};
};

View File

@@ -4,6 +4,7 @@ import { Color } from '@signozhq/design-tokens';
import type { TableColumnsType as ColumnsType } from 'antd';
import { Card, Tooltip, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
import classNames from 'classnames';
import ResizeTable from 'components/ResizeTable/ResizeTable';
import { DataType } from 'container/LogDetailedView/TableView';
@@ -14,7 +15,6 @@ import {
SPACE_AGGREGATION_OPTIONS_FOR_EXPANDED_VIEW,
TIME_AGGREGATION_OPTIONS,
} from './constants';
import { InspectMetricsSeries } from './types';
import {
ExpandedViewProps,
InspectionStep,
@@ -42,8 +42,7 @@ function ExpandedView({
useEffect(() => {
logEvent(MetricsExplorerEvents.InspectPointClicked, {
[MetricsExplorerEventKeys.Modal]: 'inspect',
[MetricsExplorerEventKeys.Filters]:
metricInspectionAppliedOptions.filterExpression,
[MetricsExplorerEventKeys.Filters]: metricInspectionAppliedOptions.filters,
[MetricsExplorerEventKeys.TimeAggregationInterval]:
metricInspectionAppliedOptions.timeAggregationInterval,
[MetricsExplorerEventKeys.TimeAggregationOption]:

View File

@@ -3,7 +3,7 @@ import * as Sentry from '@sentry/react';
import { Color } from '@signozhq/design-tokens';
import { Button, Drawer, Empty, Skeleton, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { useGetMetricMetadata } from 'api/generated/services/metrics';
import { useGetMetricDetails } from 'hooks/metricsExplorer/useGetMetricDetails';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { useIsDarkMode } from 'hooks/useDarkMode';
@@ -48,12 +48,10 @@ function Inspect({
] = useState<GraphPopoverOptions | null>(null);
const [showExpandedView, setShowExpandedView] = useState(false);
const { data: metricDetailsData } = useGetMetricMetadata(
{ metricName: appliedMetricName ?? '' },
const { data: metricDetailsData } = useGetMetricDetails(
appliedMetricName ?? '',
{
query: {
enabled: !!appliedMetricName,
},
enabled: !!appliedMetricName,
},
);
@@ -95,6 +93,7 @@ function Inspect({
const {
inspectMetricsTimeSeries,
inspectMetricsStatusCode,
isInspectMetricsLoading,
isInspectMetricsError,
formattedInspectMetricsTimeSeries,
@@ -119,13 +118,15 @@ function Inspect({
[dispatchMetricInspectionOptions],
);
const selectedMetricType = useMemo(() => metricDetailsData?.data?.type, [
metricDetailsData,
]);
const selectedMetricType = useMemo(
() => metricDetailsData?.payload?.data?.metadata?.metric_type,
[metricDetailsData],
);
const selectedMetricUnit = useMemo(() => metricDetailsData?.data?.unit, [
metricDetailsData,
]);
const selectedMetricUnit = useMemo(
() => metricDetailsData?.payload?.data?.metadata?.unit,
[metricDetailsData],
);
const aggregateAttribute = useMemo(
() => ({
@@ -179,8 +180,11 @@ function Inspect({
);
}
if (isInspectMetricsError) {
const errorMessage = 'Error loading inspect metrics.';
if (isInspectMetricsError || inspectMetricsStatusCode !== 200) {
const errorMessage =
inspectMetricsStatusCode === 400
? 'The time range is too large. Please modify it to be within 30 minutes.'
: 'Error loading inspect metrics.';
return (
<div
@@ -257,6 +261,7 @@ function Inspect({
isInspectMetricsLoading,
isInspectMetricsRefetching,
isInspectMetricsError,
inspectMetricsStatusCode,
inspectMetricsTimeSeries,
aggregatedTimeSeries,
formattedInspectMetricsTimeSeries,

View File

@@ -33,7 +33,7 @@ function MetricFilters({
});
dispatchMetricInspectionOptions({
type: 'SET_FILTERS',
payload: expression,
payload: tagFilter,
});
},
[currentQuery, dispatchMetricInspectionOptions, setCurrentQuery],

View File

@@ -1,8 +1,8 @@
import { useCallback, useMemo } from 'react';
import type { TableColumnsType as ColumnsType } from 'antd';
import { Card, Flex, Table, Typography } from 'antd';
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
import { InspectMetricsSeries } from './types';
import { TableViewProps } from './types';
import { formatTimestampToFullDateTime } from './utils';

View File

@@ -1,11 +1,11 @@
import { render, screen } from '@testing-library/react';
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
import {
SPACE_AGGREGATION_OPTIONS_FOR_EXPANDED_VIEW,
TIME_AGGREGATION_OPTIONS,
} from '../constants';
import ExpandedView from '../ExpandedView';
import { InspectMetricsSeries } from '../types';
import {
GraphPopoverData,
InspectionStep,
@@ -25,6 +25,7 @@ describe('ExpandedView', () => {
labels: {
host_id: 'test-id',
},
labelsArray: [],
title: 'TS1',
};
@@ -65,7 +66,10 @@ describe('ExpandedView', () => {
timeAggregationInterval: 60,
spaceAggregationOption: SpaceAggregationOptions.MAX_BY,
spaceAggregationLabels: ['host_name'],
filterExpression: '',
filters: {
items: [],
op: 'AND',
},
};
it('renders entire time series for a raw data inspection', () => {

View File

@@ -1,7 +1,7 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
import GraphPopover from '../GraphPopover';
import { InspectMetricsSeries } from '../types';
import { GraphPopoverOptions, InspectionStep } from '../types';
describe('GraphPopover', () => {
@@ -16,6 +16,7 @@ describe('GraphPopover', () => {
{ timestamp: 1672531260000, value: '43.456' },
],
labels: {},
labelsArray: [],
},
};
const mockSpaceAggregationSeriesMap: Map<

View File

@@ -2,12 +2,12 @@
import { Provider } from 'react-redux';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import store from 'store';
import { AlignedData } from 'uplot';
import GraphView from '../GraphView';
import { InspectMetricsSeries } from '../types';
import {
InspectionStep,
SpaceAggregationOptions,
@@ -32,6 +32,7 @@ describe('GraphView', () => {
{ timestamp: 1234567891000, value: '20' },
],
labels: { label1: 'value1' },
labelsArray: [{ label: 'label1', value: 'value1' }],
},
];
@@ -43,7 +44,7 @@ describe('GraphView', () => {
] as AlignedData,
metricUnit: '',
metricName: 'test_metric',
metricType: MetrictypesTypeDTO.gauge,
metricType: MetricType.GAUGE,
spaceAggregationSeriesMap: new Map(),
inspectionStep: InspectionStep.COMPLETED,
setPopoverOptions: jest.fn(),
@@ -57,7 +58,10 @@ describe('GraphView', () => {
spaceAggregationOption: SpaceAggregationOptions.MAX_BY,
spaceAggregationLabels: ['host_name'],
timeAggregationOption: TimeAggregationOptions.MAX,
filterExpression: '',
filters: {
items: [],
op: 'AND',
},
},
isInspectMetricsRefetching: false,
};

View File

@@ -2,21 +2,17 @@ import { QueryClient, QueryClientProvider } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { Provider } from 'react-redux';
import { render, screen } from '@testing-library/react';
import * as metricsGeneratedAPI from 'api/generated/services/metrics';
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import * as useInspectMetricsHooks from 'hooks/metricsExplorer/useGetInspectMetricsDetails';
import * as useGetMetricDetailsHooks from 'hooks/metricsExplorer/useGetMetricDetails';
import * as appContextHooks from 'providers/App/App';
import store from 'store';
import ROUTES from '../../../../constants/routes';
import { LicenseEvent } from '../../../../types/api/licensesV3/getActive';
import { INITIAL_INSPECT_METRICS_OPTIONS } from '../constants';
import Inspect from '../Inspect';
import {
InspectionStep,
InspectMetricsSeries,
UseInspectMetricsReturnData,
} from '../types';
import * as useInspectMetricsModule from '../useInspectMetrics';
import { InspectionStep } from '../types';
const queryClient = new QueryClient();
const mockTimeSeries: InspectMetricsSeries[] = [
@@ -28,6 +24,7 @@ const mockTimeSeries: InspectMetricsSeries[] = [
{ timestamp: 1234567891000, value: '20' },
],
labels: { label1: 'value1' },
labelsArray: [{ label: 'label1', value: 'value1' }],
},
];
@@ -55,19 +52,29 @@ jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
},
} as any);
jest.spyOn(metricsGeneratedAPI, 'useGetMetricMetadata').mockReturnValue({
jest.spyOn(useGetMetricDetailsHooks, 'useGetMetricDetails').mockReturnValue({
data: {
data: {
type: MetrictypesTypeDTO.gauge,
unit: '',
description: '',
temporality: '',
isMonotonic: false,
metricDetails: {
metricName: 'test_metric',
metricType: MetricType.GAUGE,
},
status: 'success',
},
} as any);
jest
.spyOn(useInspectMetricsHooks, 'useGetInspectMetricsDetails')
.mockReturnValue({
data: {
payload: {
data: {
series: mockTimeSeries,
},
status: 'success',
},
},
isLoading: false,
} as any);
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
@@ -83,25 +90,16 @@ mockResizeObserver.mockImplementation(() => ({
}));
window.ResizeObserver = mockResizeObserver;
const baseHookReturn: UseInspectMetricsReturnData = {
inspectMetricsTimeSeries: [],
isInspectMetricsLoading: false,
isInspectMetricsError: false,
formattedInspectMetricsTimeSeries: [[], []],
spaceAggregationLabels: [],
metricInspectionOptions: INITIAL_INSPECT_METRICS_OPTIONS,
dispatchMetricInspectionOptions: jest.fn(),
inspectionStep: InspectionStep.COMPLETED,
isInspectMetricsRefetching: false,
spaceAggregatedSeriesMap: new Map(),
aggregatedTimeSeries: [],
timeAggregatedSeriesMap: new Map(),
reset: jest.fn(),
};
describe('Inspect', () => {
const defaultProps = {
inspectMetricsTimeSeries: mockTimeSeries,
formattedInspectMetricsTimeSeries: [],
metricUnit: '',
metricName: 'test_metric',
metricType: MetricType.GAUGE,
spaceAggregationSeriesMap: new Map(),
inspectionStep: InspectionStep.COMPLETED,
resetInspection: jest.fn(),
isOpen: true,
onClose: jest.fn(),
};
@@ -111,12 +109,6 @@ describe('Inspect', () => {
});
it('renders all components', () => {
jest.spyOn(useInspectMetricsModule, 'useInspectMetrics').mockReturnValue({
...baseHookReturn,
inspectMetricsTimeSeries: mockTimeSeries,
aggregatedTimeSeries: mockTimeSeries,
});
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
@@ -131,11 +123,18 @@ describe('Inspect', () => {
});
it('renders loading state', () => {
jest.spyOn(useInspectMetricsModule, 'useInspectMetrics').mockReturnValue({
...baseHookReturn,
isInspectMetricsLoading: true,
});
jest
.spyOn(useInspectMetricsHooks, 'useGetInspectMetricsDetails')
.mockReturnValue({
data: {
payload: {
data: {
series: [],
},
},
},
isLoading: true,
} as any);
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
@@ -148,11 +147,18 @@ describe('Inspect', () => {
});
it('renders empty state', () => {
jest.spyOn(useInspectMetricsModule, 'useInspectMetrics').mockReturnValue({
...baseHookReturn,
inspectMetricsTimeSeries: [],
});
jest
.spyOn(useInspectMetricsHooks, 'useGetInspectMetricsDetails')
.mockReturnValue({
data: {
payload: {
data: {
series: [],
},
},
},
isLoading: false,
} as any);
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
@@ -165,11 +171,39 @@ describe('Inspect', () => {
});
it('renders error state', () => {
jest.spyOn(useInspectMetricsModule, 'useInspectMetrics').mockReturnValue({
...baseHookReturn,
isInspectMetricsError: true,
});
jest
.spyOn(useInspectMetricsHooks, 'useGetInspectMetricsDetails')
.mockReturnValue({
data: {
payload: {
data: {
series: [],
},
},
},
isLoading: false,
isError: true,
} as any);
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<Inspect {...defaultProps} />
</Provider>
</QueryClientProvider>,
);
expect(screen.getByTestId('inspect-metrics-error')).toBeInTheDocument();
});
it('renders error state with 400 status code', () => {
jest
.spyOn(useInspectMetricsHooks, 'useGetInspectMetricsDetails')
.mockReturnValue({
data: {
statusCode: 400,
},
isError: false,
} as any);
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>

View File

@@ -4,7 +4,7 @@ import { Provider } from 'react-redux';
import { fireEvent, render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as metricsService from 'api/generated/services/metrics';
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import * as appContextHooks from 'providers/App/App';
import store from 'store';
@@ -89,10 +89,13 @@ describe('QueryBuilder', () => {
timeAggregationOption: TimeAggregationOptions.AVG,
spaceAggregationLabels: [],
spaceAggregationOption: SpaceAggregationOptions.AVG_BY,
filterExpression: '',
filters: {
items: [],
op: 'and',
},
},
dispatchMetricInspectionOptions: jest.fn(),
metricType: MetrictypesTypeDTO.sum,
metricType: MetricType.SUM,
inspectionStep: InspectionStep.TIME_AGGREGATION,
inspectMetricsTimeSeries: [],
currentQuery: {
@@ -100,7 +103,6 @@ describe('QueryBuilder', () => {
items: [],
op: 'and',
},
filterExpression: '',
} as any,
setCurrentQuery: jest.fn(),
};

View File

@@ -1,7 +1,7 @@
import { render, screen } from '@testing-library/react';
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
import TableView from '../TableView';
import { InspectMetricsSeries } from '../types';
import {
InspectionStep,
SpaceAggregationOptions,
@@ -19,6 +19,12 @@ describe('TableView', () => {
{ timestamp: 1234567891000, value: '20' },
],
labels: { label1: 'value1' },
labelsArray: [
{
label: 'label1',
value: 'value1',
},
],
},
{
strokeColor: '#fff',
@@ -28,6 +34,12 @@ describe('TableView', () => {
{ timestamp: 1234567891000, value: '40' },
],
labels: { label2: 'value2' },
labelsArray: [
{
label: 'label2',
value: 'value2',
},
],
},
];
@@ -41,7 +53,10 @@ describe('TableView', () => {
timeAggregationOption: TimeAggregationOptions.MAX,
spaceAggregationOption: SpaceAggregationOptions.MAX_BY,
spaceAggregationLabels: ['host_name'],
filterExpression: '',
filters: {
items: [],
op: 'AND',
},
},
isInspectMetricsRefetching: false,
};

View File

@@ -1,6 +1,6 @@
import { ForwardRefExoticComponent, RefAttributes } from 'react';
import { Color } from '@signozhq/design-tokens';
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import {
BarChart,
BarChart2,
@@ -18,25 +18,25 @@ import {
export const INSPECT_FEATURE_FLAG_KEY = 'metrics-explorer-inspect-feature-flag';
export const METRIC_TYPE_TO_COLOR_MAP: Record<MetrictypesTypeDTO, string> = {
[MetrictypesTypeDTO.gauge]: Color.BG_SAKURA_500,
[MetrictypesTypeDTO.histogram]: Color.BG_SIENNA_500,
[MetrictypesTypeDTO.sum]: Color.BG_ROBIN_500,
[MetrictypesTypeDTO.summary]: Color.BG_FOREST_500,
[MetrictypesTypeDTO.exponentialhistogram]: Color.BG_AQUA_500,
export const METRIC_TYPE_TO_COLOR_MAP: Record<MetricType, string> = {
[MetricType.GAUGE]: Color.BG_SAKURA_500,
[MetricType.HISTOGRAM]: Color.BG_SIENNA_500,
[MetricType.SUM]: Color.BG_ROBIN_500,
[MetricType.SUMMARY]: Color.BG_FOREST_500,
[MetricType.EXPONENTIAL_HISTOGRAM]: Color.BG_AQUA_500,
};
export const METRIC_TYPE_TO_ICON_MAP: Record<
MetrictypesTypeDTO,
MetricType,
ForwardRefExoticComponent<
Omit<LucideProps, 'ref'> & RefAttributes<SVGSVGElement>
>
> = {
[MetrictypesTypeDTO.gauge]: Gauge,
[MetrictypesTypeDTO.histogram]: BarChart2,
[MetrictypesTypeDTO.sum]: Diff,
[MetrictypesTypeDTO.summary]: BarChartHorizontal,
[MetrictypesTypeDTO.exponentialhistogram]: BarChart,
[MetricType.GAUGE]: Gauge,
[MetricType.HISTOGRAM]: BarChart2,
[MetricType.SUM]: Diff,
[MetricType.SUMMARY]: BarChartHorizontal,
[MetricType.EXPONENTIAL_HISTOGRAM]: BarChart,
};
export const TIME_AGGREGATION_OPTIONS: Record<
@@ -77,14 +77,20 @@ export const INITIAL_INSPECT_METRICS_OPTIONS: MetricInspectionState = {
timeAggregationInterval: undefined,
spaceAggregationOption: undefined,
spaceAggregationLabels: [],
filterExpression: '',
filters: {
items: [],
op: 'AND',
},
},
appliedOptions: {
timeAggregationOption: undefined,
timeAggregationInterval: undefined,
spaceAggregationOption: undefined,
spaceAggregationLabels: [],
filterExpression: '',
filters: {
items: [],
op: 'AND',
},
},
};

View File

@@ -1,19 +1,11 @@
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import {
IBuilderQuery,
TagFilter,
} from 'types/api/queryBuilder/queryBuilderData';
import { AlignedData } from 'uplot';
export interface InspectMetricsTimestampValue {
timestamp: number;
value: string;
}
export interface InspectMetricsSeries {
title?: string;
strokeColor?: string;
labels: Record<string, string>;
values: InspectMetricsTimestampValue[];
}
export type InspectProps = {
metricName: string;
isOpen: boolean;
@@ -22,6 +14,7 @@ export type InspectProps = {
export interface UseInspectMetricsReturnData {
inspectMetricsTimeSeries: InspectMetricsSeries[];
inspectMetricsStatusCode: number;
isInspectMetricsLoading: boolean;
isInspectMetricsError: boolean;
formattedInspectMetricsTimeSeries: AlignedData;
@@ -40,7 +33,7 @@ export interface GraphViewProps {
inspectMetricsTimeSeries: InspectMetricsSeries[];
metricUnit: string | undefined;
metricName: string | null;
metricType?: MetrictypesTypeDTO | undefined;
metricType?: MetricType | undefined;
formattedInspectMetricsTimeSeries: AlignedData;
resetInspection: () => void;
spaceAggregationSeriesMap: Map<string, InspectMetricsSeries[]>;
@@ -113,7 +106,7 @@ export interface MetricInspectionOptions {
timeAggregationInterval: number | undefined;
spaceAggregationOption: SpaceAggregationOptions | undefined;
spaceAggregationLabels: string[];
filterExpression: string;
filters: TagFilter;
}
export interface MetricInspectionState {
@@ -126,7 +119,7 @@ export type MetricInspectionAction =
| { type: 'SET_TIME_AGGREGATION_INTERVAL'; payload: number }
| { type: 'SET_SPACE_AGGREGATION_OPTION'; payload: SpaceAggregationOptions }
| { type: 'SET_SPACE_AGGREGATION_LABELS'; payload: string[] }
| { type: 'SET_FILTERS'; payload: string }
| { type: 'SET_FILTERS'; payload: TagFilter }
| { type: 'RESET_INSPECTION' }
| { type: 'APPLY_METRIC_INSPECTION_OPTIONS' };

View File

@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useReducer, useState } from 'react';
import { useQuery } from 'react-query';
import { inspectMetrics } from 'api/generated/services/metrics';
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
import { themeColors } from 'constants/theme';
import { useGetInspectMetricsDetails } from 'hooks/metricsExplorer/useGetInspectMetricsDetails';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
@@ -9,7 +9,6 @@ import { INITIAL_INSPECT_METRICS_OPTIONS } from './constants';
import {
GraphPopoverData,
InspectionStep,
InspectMetricsSeries,
MetricInspectionAction,
MetricInspectionState,
UseInspectMetricsReturnData,
@@ -62,7 +61,7 @@ const metricInspectionReducer = (
...state,
currentOptions: {
...state.currentOptions,
filterExpression: action.payload,
filters: action.payload,
},
};
case 'APPLY_METRIC_INSPECTION_OPTIONS':
@@ -101,58 +100,26 @@ export function useInspectMetrics(
);
const {
data: inspectMetricsResponse,
data: inspectMetricsData,
isLoading: isInspectMetricsLoading,
isError: isInspectMetricsError,
isRefetching: isInspectMetricsRefetching,
} = useQuery({
queryKey: [
'inspectMetrics',
metricName,
} = useGetInspectMetricsDetails(
{
metricName: metricName ?? '',
start,
end,
metricInspectionOptions.appliedOptions.filterExpression,
],
queryFn: ({ signal }) =>
inspectMetrics(
{
metricName: metricName ?? '',
start,
end,
filter: metricInspectionOptions.appliedOptions.filterExpression
? { expression: metricInspectionOptions.appliedOptions.filterExpression }
: undefined,
},
signal,
),
enabled: !!metricName,
keepPreviousData: true,
});
const inspectMetricsData = useMemo(
() => ({
series: (inspectMetricsResponse?.data?.series ?? []).map((s) => {
const labels: Record<string, string> = {};
for (const l of s.labels ?? []) {
if (l.key?.name) {
labels[l.key.name] = String(l.value ?? '');
}
}
return {
labels,
values: (s.values ?? []).map((v) => ({
timestamp: v.timestamp ?? 0,
value: String(v.value ?? 0),
})),
};
}) as InspectMetricsSeries[],
}),
[inspectMetricsResponse],
filters: metricInspectionOptions.appliedOptions.filters,
},
{
enabled: !!metricName,
keepPreviousData: true,
},
);
const isDarkMode = useIsDarkMode();
const inspectMetricsTimeSeries = useMemo(() => {
const series = inspectMetricsData?.series ?? [];
const series = inspectMetricsData?.payload?.data?.series ?? [];
return series.map((series, index) => {
const title = `TS${index + 1}`;
@@ -169,6 +136,11 @@ export function useInspectMetrics(
});
}, [inspectMetricsData, isDarkMode]);
const inspectMetricsStatusCode = useMemo(
() => inspectMetricsData?.statusCode || 200,
[inspectMetricsData],
);
// Evaluate inspection step
const currentInspectionStep = useMemo(() => {
if (metricInspectionOptions.currentOptions.spaceAggregationOption) {
@@ -259,7 +231,7 @@ export function useInspectMetrics(
const spaceAggregationLabels = useMemo(() => {
const labels = new Set<string>();
inspectMetricsData?.series?.forEach((series) => {
inspectMetricsData?.payload?.data.series.forEach((series) => {
Object.keys(series.labels).forEach((label) => {
labels.add(label);
});
@@ -278,6 +250,7 @@ export function useInspectMetrics(
return {
inspectMetricsTimeSeries,
inspectMetricsStatusCode,
isInspectMetricsLoading,
isInspectMetricsError,
formattedInspectMetricsTimeSeries,

View File

@@ -1,7 +1,7 @@
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { InspectMetricsSeries } from './types';
import {
GraphPopoverData,
GraphPopoverOptions,

View File

@@ -5,6 +5,7 @@ import {
MetrictypesTemporalityDTO,
MetrictypesTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
import {
UniversalYAxisUnit,
YAxisUnitSelectorProps,
@@ -179,10 +180,7 @@ describe('Metadata', () => {
const temporalitySelect = screen.getByTestId('temporality-select');
expect(temporalitySelect).toBeInTheDocument();
await userEvent.selectOptions(
temporalitySelect,
MetrictypesTemporalityDTO.cumulative,
);
await userEvent.selectOptions(temporalitySelect, Temporality.CUMULATIVE);
const unitSelect = screen.getByTestId('unit-select');
expect(unitSelect).toBeInTheDocument();

View File

@@ -9,17 +9,16 @@ import {
Popover,
Spin,
} from 'antd';
import {
getListMetricsQueryKey,
useListMetrics,
} from 'api/generated/services/metrics';
import { Filter } from 'api/v5/v5';
import {
convertExpressionToFilters,
convertFiltersToExpression,
} from 'components/QueryBuilderV2/utils';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useGetMetricsListFilterValues } from 'hooks/metricsExplorer/useGetMetricsListFilterValues';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { Search } from 'lucide-react';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
function MetricNameSearch({
queryFilterExpression,
@@ -45,27 +44,25 @@ function MetricNameSearch({
}, [isPopoverOpen]);
const {
data: metricNameListData,
data: metricNameFilterValuesData,
isLoading: isLoadingMetricNameFilterValues,
isError: isErrorMetricNameFilterValues,
} = useListMetrics(
} = useGetMetricsListFilterValues(
{
searchText: debouncedSearchString,
filterKey: 'metric_name',
filterAttributeKeyDataType: DataTypes.String,
limit: 10,
},
{
query: {
enabled: isPopoverOpen,
refetchOnWindowFocus: false,
queryKey: [
...getListMetricsQueryKey({
searchText: debouncedSearchString,
limit: 10,
}),
'search',
isPopoverOpen,
],
},
enabled: isPopoverOpen,
refetchOnWindowFocus: false,
queryKey: [
REACT_QUERY_KEY.GET_METRICS_LIST_FILTER_VALUES,
'metric_name',
debouncedSearchString,
isPopoverOpen,
],
},
);
@@ -98,8 +95,8 @@ function MetricNameSearch({
);
const metricNameFilterValues = useMemo(
() => metricNameListData?.data?.metrics?.map((m) => m.metricName) || [],
[metricNameListData],
() => metricNameFilterValuesData?.payload?.data?.filterValues || [],
[metricNameFilterValuesData],
);
const handleKeyDown = useCallback(

View File

@@ -2,8 +2,8 @@
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { fireEvent, render, screen } from '@testing-library/react';
import * as metricsGeneratedAPI from 'api/generated/services/metrics';
import { Filter } from 'api/v5/v5';
import * as useGetMetricsListFilterValues from 'hooks/metricsExplorer/useGetMetricsListFilterValues';
import * as useQueryBuilderOperationsHooks from 'hooks/queryBuilder/useQueryBuilderOperations';
import store from 'store';
import APIError from 'types/api/error';
@@ -53,33 +53,21 @@ describe('MetricsTable', () => {
} as any);
});
jest.spyOn(metricsGeneratedAPI, 'useListMetrics').mockReturnValue(({
data: {
jest
.spyOn(useGetMetricsListFilterValues, 'useGetMetricsListFilterValues')
.mockReturnValue({
data: {
metrics: [
{
metricName: 'metric1',
description: '',
type: '',
unit: '',
temporality: '',
isMonotonic: false,
statusCode: 200,
payload: {
status: 'success',
data: {
filterValues: ['metric1', 'metric2'],
},
{
metricName: 'metric2',
description: '',
type: '',
unit: '',
temporality: '',
isMonotonic: false,
},
],
},
},
status: 'success',
},
isLoading: false,
isError: false,
} as unknown) as ReturnType<typeof metricsGeneratedAPI.useListMetrics>);
isLoading: false,
isError: false,
} as any);
it('renders table with data correctly', () => {
render(

View File

@@ -6,6 +6,7 @@ import {
MetricsexplorertypesTreemapEntryDTO,
MetricsexplorertypesTreemapModeDTO,
} from 'api/generated/services/sigNoz.schemas';
import { MetricsListPayload } from 'api/metricsExplorer/getMetricsList';
import { Filter } from 'api/v5/v5';
import { getUniversalNameFromMetricUnit } from 'components/YAxisUnitSelector/utils';
@@ -75,6 +76,14 @@ export const getMetricsTableColumns = (
},
];
export const getMetricsListQuery = (): MetricsListPayload => ({
filters: {
items: [],
op: 'and',
},
orderBy: { columnName: 'metric_name', order: 'asc' },
});
function ValidateRowValueWrapper({
value,
children,

View File

@@ -1,5 +1,5 @@
import { render, screen } from '@testing-library/react';
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { ReduceOperators } from 'types/common/queryBuilder';
@@ -47,7 +47,7 @@ describe('ReduceToFilter', () => {
spaceAggregation: 'sum',
},
],
aggregateAttribute: { key: 'test', type: MetrictypesTypeDTO.sum },
aggregateAttribute: { key: 'test', type: MetricType.SUM },
})}
onChange={mockOnChange}
/>,
@@ -61,7 +61,7 @@ describe('ReduceToFilter', () => {
<ReduceToFilter
query={baseQuery({
reduceTo: ReduceOperators.MAX,
aggregateAttribute: { key: 'test', type: MetrictypesTypeDTO.gauge },
aggregateAttribute: { key: 'test', type: MetricType.GAUGE },
})}
onChange={mockOnChange}
/>,

View File

@@ -0,0 +1,55 @@
import { useMemo } from 'react';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import {
getInspectMetricsDetails,
InspectMetricsRequest,
InspectMetricsResponse,
} from 'api/metricsExplorer/getInspectMetricsDetails';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { ErrorResponse, SuccessResponse } from 'types/api';
type UseGetInspectMetricsDetails = (
requestData: InspectMetricsRequest,
options?: UseQueryOptions<
SuccessResponse<InspectMetricsResponse> | ErrorResponse,
Error
>,
headers?: Record<string, string>,
) => UseQueryResult<
SuccessResponse<InspectMetricsResponse> | ErrorResponse,
Error
>;
export const useGetInspectMetricsDetails: UseGetInspectMetricsDetails = (
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_INSPECT_METRICS_DETAILS,
requestData.metricName,
requestData.start,
requestData.end,
requestData.filters,
];
}, [options?.queryKey, requestData]);
return useQuery<
SuccessResponse<InspectMetricsResponse> | ErrorResponse,
Error
>({
queryFn: ({ signal }) =>
getInspectMetricsDetails(requestData, signal, headers),
...options,
queryKey,
});
};

View File

@@ -0,0 +1,46 @@
import { useMemo } from 'react';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import {
getMetricDetails,
MetricDetailsResponse,
} from 'api/metricsExplorer/getMetricDetails';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { ErrorResponse, SuccessResponse } from 'types/api';
type UseGetMetricDetails = (
metricName: string,
options?: UseQueryOptions<
SuccessResponse<MetricDetailsResponse> | ErrorResponse,
Error
>,
headers?: Record<string, string>,
) => UseQueryResult<
SuccessResponse<MetricDetailsResponse> | ErrorResponse,
Error
>;
export const useGetMetricDetails: UseGetMetricDetails = (
metricName,
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_METRIC_DETAILS, metricName];
}, [options?.queryKey, metricName]);
return useQuery<SuccessResponse<MetricDetailsResponse> | ErrorResponse, Error>(
{
queryFn: ({ signal }) => getMetricDetails(metricName, signal, headers),
...options,
queryKey,
},
);
};

View File

@@ -0,0 +1,47 @@
import { useMemo } from 'react';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import {
getMetricsList,
MetricsListPayload,
MetricsListResponse,
} from 'api/metricsExplorer/getMetricsList';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { ErrorResponse, SuccessResponse } from 'types/api';
type UseGetMetricsList = (
requestData: MetricsListPayload,
options?: UseQueryOptions<
SuccessResponse<MetricsListResponse> | ErrorResponse,
Error
>,
headers?: Record<string, string>,
) => UseQueryResult<
SuccessResponse<MetricsListResponse> | ErrorResponse,
Error
>;
export const useGetMetricsList: UseGetMetricsList = (
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_METRICS_LIST, requestData];
}, [options?.queryKey, requestData]);
return useQuery<SuccessResponse<MetricsListResponse> | ErrorResponse, Error>({
queryFn: ({ signal }) => getMetricsList(requestData, signal, headers),
...options,
queryKey,
});
};

View File

@@ -0,0 +1,48 @@
import { useMemo } from 'react';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import {
getMetricsListFilterKeys,
GetMetricsListFilterKeysParams,
MetricsListFilterKeysResponse,
} from 'api/metricsExplorer/getMetricsListFilterKeys';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { ErrorResponse, SuccessResponse } from 'types/api';
type UseGetMetricsListFilterKeys = (
params: GetMetricsListFilterKeysParams,
options?: UseQueryOptions<
SuccessResponse<MetricsListFilterKeysResponse> | ErrorResponse,
Error
>,
headers?: Record<string, string>,
) => UseQueryResult<
SuccessResponse<MetricsListFilterKeysResponse> | ErrorResponse,
Error
>;
export const useGetMetricsListFilterKeys: UseGetMetricsListFilterKeys = (
params,
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_METRICS_LIST_FILTER_KEYS];
}, [options?.queryKey]);
return useQuery<
SuccessResponse<MetricsListFilterKeysResponse> | ErrorResponse,
Error
>({
queryFn: ({ signal }) => getMetricsListFilterKeys(params, signal, headers),
...options,
queryKey,
});
};

View File

@@ -0,0 +1,42 @@
import { useMemo } from 'react';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import {
getMetricsListFilterValues,
MetricsListFilterValuesPayload,
MetricsListFilterValuesResponse,
} from 'api/metricsExplorer/getMetricsListFilterValues';
import { ErrorResponse, SuccessResponse } from 'types/api';
type UseGetMetricsListFilterValues = (
payload: MetricsListFilterValuesPayload,
options?: UseQueryOptions<
SuccessResponse<MetricsListFilterValuesResponse> | ErrorResponse,
Error
>,
headers?: Record<string, string>,
) => UseQueryResult<
SuccessResponse<MetricsListFilterValuesResponse> | ErrorResponse,
Error
>;
export const useGetMetricsListFilterValues: UseGetMetricsListFilterValues = (
props,
options,
headers,
) => {
const queryKey = useMemo(() => {
if (options?.queryKey && Array.isArray(options.queryKey)) {
return [...options.queryKey];
}
return [props];
}, [options?.queryKey, props]);
return useQuery<
SuccessResponse<MetricsListFilterValuesResponse> | ErrorResponse,
Error
>({
queryFn: ({ signal }) => getMetricsListFilterValues(props, signal, headers),
...options,
queryKey,
});
};

View File

@@ -0,0 +1,48 @@
import { useMemo } from 'react';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import {
getRelatedMetrics,
RelatedMetricsPayload,
RelatedMetricsResponse,
} from 'api/metricsExplorer/getRelatedMetrics';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { ErrorResponse, SuccessResponse } from 'types/api';
type UseGetRelatedMetrics = (
requestData: RelatedMetricsPayload,
options?: UseQueryOptions<
SuccessResponse<RelatedMetricsResponse> | ErrorResponse,
Error
>,
headers?: Record<string, string>,
) => UseQueryResult<
SuccessResponse<RelatedMetricsResponse> | ErrorResponse,
Error
>;
export const useGetRelatedMetrics: UseGetRelatedMetrics = (
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_RELATED_METRICS, requestData];
}, [options?.queryKey, requestData]);
return useQuery<
SuccessResponse<RelatedMetricsResponse> | ErrorResponse,
Error
>({
queryFn: ({ signal }) => getRelatedMetrics(requestData, signal, headers),
...options,
queryKey,
});
};

View File

@@ -1,6 +1,7 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useDebounce } from 'react-use';
import { getMetricsListFilterValues } from 'api/metricsExplorer/getMetricsListFilterValues';
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
import { DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY } from 'constants/queryBuilder';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
@@ -13,6 +14,7 @@ import {
getTagToken,
isInNInOperator,
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { useGetMetricsListFilterKeys } from 'hooks/metricsExplorer/useGetMetricsListFilterKeys';
import useDebounceValue from 'hooks/useDebounce';
import { cloneDeep, isEqual, uniqWith, unset } from 'lodash-es';
import { IAttributeValuesResponse } from 'types/api/queryBuilder/getAttributesValues';
@@ -150,8 +152,21 @@ export const useFetchKeysAndValues = (
},
);
const {
data: metricsListFilterKeysData,
isFetching: isFetchingMetricsListFilterKeys,
status: fetchingMetricsListFilterKeysStatus,
} = useGetMetricsListFilterKeys(
{
searchText: searchKey,
},
{
enabled: isMetricsExplorer && isQueryEnabled && !shouldUseSuggestions,
queryKey: [searchKey],
},
);
function isAttributeValuesResponse(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
payload: any,
): payload is IAttributeValuesResponse {
return (
@@ -165,6 +180,14 @@ export const useFetchKeysAndValues = (
);
}
function isMetricsListFilterValuesData(
payload: any,
): payload is { filterValues: string[] } {
return (
payload && 'filterValues' in payload && Array.isArray(payload.filterValues)
);
}
/**
* Fetches the options to be displayed based on the selected value
* @param value - the selected value
@@ -208,6 +231,15 @@ export const useFetchKeysAndValues = (
: tagValue?.toString() ?? '',
});
payload = response.payload;
} else if (isMetricsExplorer) {
const response = await getMetricsListFilterValues({
searchText: searchKey,
filterKey: filterAttributeKey?.key ?? tagKey,
filterAttributeKeyDataType:
filterAttributeKey?.dataType ?? DataTypes.EMPTY,
limit: 10,
});
payload = response.payload?.data;
} else {
const response = await getAttributesValues({
aggregateOperator: query.aggregateOperator || '',
@@ -224,11 +256,18 @@ export const useFetchKeysAndValues = (
payload = response.payload;
}
if (payload && isAttributeValuesResponse(payload)) {
const dataType = filterAttributeKey?.dataType ?? DataTypes.String;
const key = DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY[dataType];
setResults(key ? payload[key] || [] : []);
return;
if (payload) {
if (isAttributeValuesResponse(payload)) {
const dataType = filterAttributeKey?.dataType ?? DataTypes.String;
const key = DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY[dataType];
setResults(key ? payload[key] || [] : []);
return;
}
if (isMetricsExplorer && isMetricsListFilterValuesData(payload)) {
setResults(payload.filterValues || []);
return;
}
}
} catch (e) {
console.error(e);
@@ -266,6 +305,32 @@ export const useFetchKeysAndValues = (
}
}, [data?.payload?.attributeKeys, status]);
useEffect(() => {
if (
isMetricsExplorer &&
fetchingMetricsListFilterKeysStatus === 'success' &&
!isFetchingMetricsListFilterKeys &&
metricsListFilterKeysData?.payload?.data?.attributeKeys
) {
setKeys(metricsListFilterKeysData.payload.data.attributeKeys);
setSourceKeys((prevState) =>
uniqWith(
[
...(metricsListFilterKeysData.payload.data.attributeKeys ?? []),
...prevState,
],
isEqual,
),
);
}
}, [
metricsListFilterKeysData?.payload?.data?.attributeKeys,
fetchingMetricsListFilterKeysStatus,
isMetricsExplorer,
metricsListFilterKeysData,
isFetchingMetricsListFilterKeys,
]);
useEffect(() => {
if (
fetchingSuggestionsStatus === 'success' &&

View File

@@ -0,0 +1,15 @@
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
export interface MetricMetadata {
description: string;
type: MetricType;
unit: string;
temporality: Temporality;
isMonotonic: boolean;
}
export interface MetricMetadataResponse {
status: string;
data: MetricMetadata;
}

4
go.mod
View File

@@ -372,7 +372,7 @@ require (
go.opentelemetry.io/otel/log v0.15.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.14.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.40.0
go.opentelemetry.io/proto/otlp v1.9.0
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/mock v0.6.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
@@ -381,7 +381,7 @@ require (
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.41.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/grpc v1.78.0 // indirect
gopkg.in/telebot.v3 v3.3.8 // indirect
k8s.io/client-go v0.35.0 // indirect

View File

@@ -47,7 +47,7 @@ type Dispatcher struct {
receiverRoutes map[string]*dispatch.Route
}
// We use the upstream Limits interface from Prometheus.
// We use the upstream Limits interface from Prometheus
type Limits = dispatch.Limits
// NewDispatcher returns a new Dispatcher.
@@ -273,7 +273,7 @@ type notifyFunc func(context.Context, ...*types.Alert) bool
// processAlert determines in which aggregation group the alert falls
// and inserts it.
// no data alert will only have ruleId and no data label.
// no data alert will only have ruleId and no data label
func (d *Dispatcher) processAlert(alert *types.Alert, route *dispatch.Route) {
ruleId := getRuleIDFromAlert(alert)
config, err := d.notificationManager.GetNotificationConfig(d.orgID, ruleId)
@@ -510,7 +510,7 @@ func (ag *aggrGroup) flush(notify func(...*types.Alert) bool) {
}
}
// unlimitedLimits provides unlimited aggregation groups for SigNoz.
// unlimitedLimits provides unlimited aggregation groups for SigNoz
type unlimitedLimits struct{}
func (u *unlimitedLimits) MaxNumberOfAggregationGroups() int { return 0 }

View File

@@ -169,20 +169,19 @@ func TestEndToEndAlertManagerFlow(t *testing.T) {
require.Len(t, alerts, 3, "Expected 3 active alerts")
for _, alert := range alerts {
require.Equal(t, "high-cpu-usage", alert.Labels["ruleId"])
require.NotEmpty(t, alert.Labels["severity"])
require.Contains(t, []string{"critical", "warning"}, alert.Labels["severity"])
require.Equal(t, "prod-cluster", alert.Labels["cluster"])
require.NotEmpty(t, alert.Labels["instance"])
require.Equal(t, "high-cpu-usage", alert.Alert.Labels["ruleId"])
require.NotEmpty(t, alert.Alert.Labels["severity"])
require.Contains(t, []string{"critical", "warning"}, alert.Alert.Labels["severity"])
require.Equal(t, "prod-cluster", alert.Alert.Labels["cluster"])
require.NotEmpty(t, alert.Alert.Labels["instance"])
}
criticalAlerts := 0
warningAlerts := 0
for _, alert := range alerts {
switch alert.Labels["severity"] {
case "critical":
if alert.Alert.Labels["severity"] == "critical" {
criticalAlerts++
case "warning":
} else if alert.Alert.Labels["severity"] == "warning" {
warningAlerts++
}
}

View File

@@ -124,7 +124,7 @@ func TestServerPutAlerts(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, 1, len(gettableAlerts))
assert.Equal(t, gettableAlerts[0].Labels["alertname"], "test-alert")
assert.Equal(t, gettableAlerts[0].Alert.Labels["alertname"], "test-alert")
assert.NoError(t, server.Stop(context.Background()))
}

View File

@@ -9,7 +9,7 @@ type DispatcherMetrics struct {
}
// NewDispatcherMetrics returns a new registered DispatchMetrics.
// todo(aniketio-ctrl): change prom metrics to otel metrics.
// todo(aniketio-ctrl): change prom metrics to otel metrics
func NewDispatcherMetrics(registerLimitMetrics bool, r prometheus.Registerer) *DispatcherMetrics {
m := DispatcherMetrics{
aggrGroups: prometheus.NewGauge(

View File

@@ -9,7 +9,7 @@ import (
"github.com/prometheus/common/model"
)
// MockNotificationManager is a simple mock implementation of NotificationManager.
// MockNotificationManager is a simple mock implementation of NotificationManager
type MockNotificationManager struct {
configs map[string]*alertmanagertypes.NotificationConfig
routes map[string]*alertmanagertypes.RoutePolicy
@@ -17,7 +17,7 @@ type MockNotificationManager struct {
errors map[string]error
}
// NewMock creates a new mock notification manager.
// NewMock creates a new mock notification manager
func NewMock() *MockNotificationManager {
return &MockNotificationManager{
configs: make(map[string]*alertmanagertypes.NotificationConfig),

View File

@@ -216,7 +216,7 @@ func (r *provider) Match(ctx context.Context, orgID string, ruleID string, set m
// the nested structure takes precedence. That means we will replace an existing leaf at any
// intermediate path with a map so we can materialize the deeper structure.
// TODO(srikanthccv): we need a better solution to handle this, remove the following
// when we update the expr to support dotted keys.
// when we update the expr to support dotted keys
func (r *provider) convertLabelSetToEnv(ctx context.Context, labelSet model.LabelSet) map[string]interface{} {
env := make(map[string]interface{})

View File

@@ -217,7 +217,7 @@ func (provider *provider) GetConfig(ctx context.Context, orgID string) (*alertma
}
func (provider *provider) SetDefaultConfig(ctx context.Context, orgID string) error {
config, err := alertmanagertypes.NewDefaultConfig(provider.config.Signoz.Global, provider.config.Signoz.Route, orgID)
config, err := alertmanagertypes.NewDefaultConfig(provider.config.Signoz.Config.Global, provider.config.Signoz.Config.Route, orgID)
if err != nil {
return err
}

View File

@@ -183,43 +183,5 @@ func (provider *provider) addMetricsExplorerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/metrics/inspect", handler.New(
provider.authZ.ViewAccess(provider.metricsExplorerHandler.InspectMetrics),
handler.OpenAPIDef{
ID: "InspectMetrics",
Tags: []string{"metrics"},
Summary: "Inspect raw metric data points",
Description: "Returns raw time series data points for a metric within a time range (max 30 minutes). Each series includes labels and timestamp/value pairs.",
Request: new(metricsexplorertypes.InspectMetricsRequest),
RequestContentType: "application/json",
Response: new(metricsexplorertypes.InspectMetricsResponse),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/metrics/onboarding", handler.New(
provider.authZ.ViewAccess(provider.metricsExplorerHandler.GetOnboardingStatus),
handler.OpenAPIDef{
ID: "GetMetricsOnboardingStatus",
Tags: []string{"metrics"},
Summary: "Check if non-SigNoz metrics have been received",
Description: "Lightweight endpoint that checks if any non-SigNoz metrics have been ingested, used for onboarding status detection",
Request: nil,
RequestContentType: "",
Response: new(metricsexplorertypes.MetricsOnboardingResponse),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusUnauthorized, http.StatusInternalServerError},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -3,18 +3,10 @@ package auditor
import (
"context"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/types/audittypes"
)
var (
ErrCodeAuditExportFailed = errors.MustNewCode("audit_export_failed")
)
type Auditor interface {
factory.ServiceWithHealthy
// Audit emits an audit event. It is fire-and-forget: callers never block on audit outcomes.
Audit(ctx context.Context, event audittypes.AuditEvent)
}

View File

@@ -1,7 +1,6 @@
package auditor
import (
"net/url"
"time"
"github.com/SigNoz/signoz/pkg/errors"
@@ -11,7 +10,6 @@ import (
var _ factory.Config = (*Config)(nil)
type Config struct {
// Provider specifies the audit export implementation to use.
Provider string `mapstructure:"provider"`
// BufferSize is the async channel capacity for audit events.
@@ -30,12 +28,18 @@ type Config struct {
// OTLPHTTPConfig holds configuration for the OTLP HTTP exporter provider.
// Fields map to go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp options.
type OTLPHTTPConfig struct {
// Endpoint is the target scheme://host:port of the OTLP HTTP endpoint.
Endpoint *url.URL `mapstructure:"endpoint"`
// Endpoint is the target host:port (without scheme or path).
Endpoint string `mapstructure:"endpoint"`
// URLPath overrides the default URL path (/v1/logs).
URLPath string `mapstructure:"url_path"`
// Insecure disables TLS, using HTTP instead of HTTPS.
Insecure bool `mapstructure:"insecure"`
// Compression sets the compression strategy. Supported: "none", "gzip".
Compression string `mapstructure:"compression"`
// Timeout is the maximum duration for an export attempt.
Timeout time.Duration `mapstructure:"timeout"`
@@ -67,12 +71,10 @@ func newConfig() factory.Config {
BatchSize: 100,
FlushInterval: time.Second,
OTLPHTTP: OTLPHTTPConfig{
Endpoint: &url.URL{
Scheme: "http",
Host: "localhost:4318",
Path: "/v1/logs",
},
Timeout: 10 * time.Second,
Endpoint: "localhost:4318",
URLPath: "/v1/logs",
Compression: "none",
Timeout: 10 * time.Second,
Retry: RetryConfig{
Enabled: true,
InitialInterval: 5 * time.Second,
@@ -91,24 +93,14 @@ func (c Config) Validate() error {
if c.BufferSize <= 0 {
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "auditor::buffer_size must be greater than 0")
}
if c.BatchSize <= 0 {
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "auditor::batch_size must be greater than 0")
}
if c.FlushInterval <= 0 {
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "auditor::flush_interval must be greater than 0")
}
if c.BatchSize > c.BufferSize {
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "auditor::batch_size must not exceed auditor::buffer_size")
}
if c.Provider == "otlphttp" {
if c.OTLPHTTP.Endpoint == nil {
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "auditor::otlphttp::endpoint must be set when provider is otlphttp")
}
}
return nil
}

View File

@@ -1,42 +0,0 @@
package noopauditor
import (
"context"
"github.com/SigNoz/signoz/pkg/auditor"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/types/audittypes"
)
type provider struct {
healthyC chan struct{}
stopC chan struct{}
}
func NewFactory() factory.ProviderFactory[auditor.Auditor, auditor.Config] {
return factory.NewProviderFactory(factory.MustNewName("noop"), New)
}
func New(ctx context.Context, providerSettings factory.ProviderSettings, config auditor.Config) (auditor.Auditor, error) {
return &provider{
healthyC: make(chan struct{}),
stopC: make(chan struct{}),
}, nil
}
func (p *provider) Start(_ context.Context) error {
close(p.healthyC)
<-p.stopC
return nil
}
func (p *provider) Stop(_ context.Context) error {
close(p.stopC)
return nil
}
func (p *provider) Healthy() <-chan struct{} {
return p.healthyC
}
func (p *provider) Audit(_ context.Context, _ audittypes.AuditEvent) {}

View File

@@ -210,7 +210,7 @@ func (a *AuthN) fetchGoogleWorkspaceGroups(ctx context.Context, userEmail string
return a.getGroups(ctx, adminService, userEmail, config.FetchTransitiveGroupMembership, checkedGroups)
}
// Recursive method.
// Recursive method
func (a *AuthN) getGroups(ctx context.Context, adminService *admin.Service, userEmail string, fetchTransitive bool, checkedGroups map[string]struct{}) ([]string, error) {
var userGroups []string
var pageToken string

View File

@@ -27,7 +27,7 @@ func NewConf() *Conf {
// NewConfFromMap creates a new Conf instance from a map.
func NewConfFromMap(m map[string]any) (*Conf, error) {
conf := NewConf()
if err := conf.Load(confmap.Provider(m, KoanfDelimiter), nil); err != nil {
if err := conf.Koanf.Load(confmap.Provider(m, KoanfDelimiter), nil); err != nil {
return nil, err
}
@@ -74,7 +74,7 @@ func (conf *Conf) Unmarshal(path string, input any, tags ...string) error {
Result: input,
}
err := conf.UnmarshalWithConf(path, input, koanf.UnmarshalConf{Tag: tag, DecoderConfig: dc})
err := conf.Koanf.UnmarshalWithConf(path, input, koanf.UnmarshalConf{Tag: tag, DecoderConfig: dc})
if err != nil {
return err
}
@@ -93,7 +93,7 @@ func (conf *Conf) Set(key string, input any) error {
}
newConf := NewConf()
if err := newConf.Load(confmap.Provider(m, KoanfDelimiter), nil); err != nil {
if err := newConf.Koanf.Load(confmap.Provider(m, KoanfDelimiter), nil); err != nil {
return err
}

View File

@@ -40,7 +40,7 @@ type WhereClauseRewriter struct {
// In this case, the Severity text will appear in the `labels` if it were part of the group
// by clause, in which case we replace it with the actual value for the notification
// i.e Severity text = WARN
// If the Severity text is not part of the group by clause, then we add it as it is.
// If the Severity text is not part of the group by clause, then we add it as it is
func PrepareFilterExpression(labels map[string]string, whereClause string, groupByItems []qbtypes.GroupByKey) string {
if whereClause == "" && len(labels) == 0 {
return ""
@@ -100,12 +100,12 @@ func PrepareFilterExpression(labels map[string]string, whereClause string, group
return rewrittenClause
}
// Visit implements the visitor for the query rule.
// Visit implements the visitor for the query rule
func (r *WhereClauseRewriter) Visit(tree antlr.ParseTree) interface{} {
return tree.Accept(r)
}
// VisitQuery visits the query node.
// VisitQuery visits the query node
func (r *WhereClauseRewriter) VisitQuery(ctx *parser.QueryContext) interface{} {
if ctx.Expression() != nil {
ctx.Expression().Accept(r)
@@ -113,7 +113,7 @@ func (r *WhereClauseRewriter) VisitQuery(ctx *parser.QueryContext) interface{} {
return nil
}
// VisitExpression visits the expression node.
// VisitExpression visits the expression node
func (r *WhereClauseRewriter) VisitExpression(ctx *parser.ExpressionContext) interface{} {
if ctx.OrExpression() != nil {
ctx.OrExpression().Accept(r)
@@ -121,7 +121,7 @@ func (r *WhereClauseRewriter) VisitExpression(ctx *parser.ExpressionContext) int
return nil
}
// VisitOrExpression visits OR expressions.
// VisitOrExpression visits OR expressions
func (r *WhereClauseRewriter) VisitOrExpression(ctx *parser.OrExpressionContext) interface{} {
for i, andExpr := range ctx.AllAndExpression() {
if i > 0 {
@@ -132,7 +132,7 @@ func (r *WhereClauseRewriter) VisitOrExpression(ctx *parser.OrExpressionContext)
return nil
}
// VisitAndExpression visits AND expressions.
// VisitAndExpression visits AND expressions
func (r *WhereClauseRewriter) VisitAndExpression(ctx *parser.AndExpressionContext) interface{} {
unaryExprs := ctx.AllUnaryExpression()
for i, unaryExpr := range unaryExprs {
@@ -150,7 +150,7 @@ func (r *WhereClauseRewriter) VisitAndExpression(ctx *parser.AndExpressionContex
return nil
}
// VisitUnaryExpression visits unary expressions (with optional NOT).
// VisitUnaryExpression visits unary expressions (with optional NOT)
func (r *WhereClauseRewriter) VisitUnaryExpression(ctx *parser.UnaryExpressionContext) interface{} {
if ctx.NOT() != nil {
r.rewritten.WriteString("NOT ")
@@ -161,7 +161,7 @@ func (r *WhereClauseRewriter) VisitUnaryExpression(ctx *parser.UnaryExpressionCo
return nil
}
// VisitPrimary visits primary expressions.
// VisitPrimary visits primary expressions
func (r *WhereClauseRewriter) VisitPrimary(ctx *parser.PrimaryContext) interface{} {
if ctx.LPAREN() != nil && ctx.RPAREN() != nil {
r.rewritten.WriteString("(")
@@ -183,7 +183,7 @@ func (r *WhereClauseRewriter) VisitPrimary(ctx *parser.PrimaryContext) interface
return nil
}
// VisitComparison visits comparison expressions.
// VisitComparison visits comparison expressions
func (r *WhereClauseRewriter) VisitComparison(ctx *parser.ComparisonContext) interface{} {
if ctx.Key() == nil {
return nil
@@ -304,7 +304,7 @@ func (r *WhereClauseRewriter) VisitComparison(ctx *parser.ComparisonContext) int
return nil
}
// VisitInClause visits IN clauses.
// VisitInClause visits IN clauses
func (r *WhereClauseRewriter) VisitInClause(ctx *parser.InClauseContext) interface{} {
r.rewritten.WriteString("IN ")
if ctx.LPAREN() != nil {
@@ -325,7 +325,7 @@ func (r *WhereClauseRewriter) VisitInClause(ctx *parser.InClauseContext) interfa
return nil
}
// VisitNotInClause visits NOT IN clauses.
// VisitNotInClause visits NOT IN clauses
func (r *WhereClauseRewriter) VisitNotInClause(ctx *parser.NotInClauseContext) interface{} {
r.rewritten.WriteString("NOT IN ")
if ctx.LPAREN() != nil {
@@ -346,7 +346,7 @@ func (r *WhereClauseRewriter) VisitNotInClause(ctx *parser.NotInClauseContext) i
return nil
}
// VisitValueList visits value lists.
// VisitValueList visits value lists
func (r *WhereClauseRewriter) VisitValueList(ctx *parser.ValueListContext) interface{} {
values := ctx.AllValue()
for i, val := range values {
@@ -358,13 +358,13 @@ func (r *WhereClauseRewriter) VisitValueList(ctx *parser.ValueListContext) inter
return nil
}
// VisitFullText visits full text expressions.
// VisitFullText visits full text expressions
func (r *WhereClauseRewriter) VisitFullText(ctx *parser.FullTextContext) interface{} {
r.rewritten.WriteString(ctx.GetText())
return nil
}
// VisitFunctionCall visits function calls.
// VisitFunctionCall visits function calls
func (r *WhereClauseRewriter) VisitFunctionCall(ctx *parser.FunctionCallContext) interface{} {
// Write function name
if ctx.HAS() != nil {
@@ -385,7 +385,7 @@ func (r *WhereClauseRewriter) VisitFunctionCall(ctx *parser.FunctionCallContext)
return nil
}
// VisitFunctionParamList visits function parameter lists.
// VisitFunctionParamList visits function parameter lists
func (r *WhereClauseRewriter) VisitFunctionParamList(ctx *parser.FunctionParamListContext) interface{} {
params := ctx.AllFunctionParam()
for i, param := range params {
@@ -397,7 +397,7 @@ func (r *WhereClauseRewriter) VisitFunctionParamList(ctx *parser.FunctionParamLi
return nil
}
// VisitFunctionParam visits function parameters.
// VisitFunctionParam visits function parameters
func (r *WhereClauseRewriter) VisitFunctionParam(ctx *parser.FunctionParamContext) interface{} {
if ctx.Key() != nil {
ctx.Key().Accept(r)
@@ -409,7 +409,7 @@ func (r *WhereClauseRewriter) VisitFunctionParam(ctx *parser.FunctionParamContex
return nil
}
// VisitArray visits array expressions.
// VisitArray visits array expressions
func (r *WhereClauseRewriter) VisitArray(ctx *parser.ArrayContext) interface{} {
r.rewritten.WriteString("[")
if ctx.ValueList() != nil {
@@ -419,13 +419,13 @@ func (r *WhereClauseRewriter) VisitArray(ctx *parser.ArrayContext) interface{} {
return nil
}
// VisitValue visits value expressions.
// VisitValue visits value expressions
func (r *WhereClauseRewriter) VisitValue(ctx *parser.ValueContext) interface{} {
r.rewritten.WriteString(ctx.GetText())
return nil
}
// VisitKey visits key expressions.
// VisitKey visits key expressions
func (r *WhereClauseRewriter) VisitKey(ctx *parser.KeyContext) interface{} {
r.keysSeen[ctx.GetText()] = struct{}{}
r.rewritten.WriteString(ctx.GetText())
@@ -438,7 +438,7 @@ func (r *WhereClauseRewriter) isKeyInWhereClause(key string) bool {
}
// escapeValueIfNeeded adds single quotes to string values and escapes single quotes within them
// Numeric and boolean values are returned as-is.
// Numeric and boolean values are returned as-is
func escapeValueIfNeeded(value string) string {
// Check if it's a number
if _, err := fmt.Sscanf(value, "%f", new(float64)); err == nil {

View File

@@ -146,7 +146,7 @@ func PrepareLinksToLogs(start, end time.Time, filterItems []v3.FilterItem) strin
// In this case, the Severity text will appear in the `lbls` if it were part of the group
// by clause, in which case we replace it with the actual value for the notification
// i.e Severity text = WARN
// If the Severity text is not part of the group by clause, then we add it as it is.
// If the Severity text is not part of the group by clause, then we add it as it is
func PrepareFilters(labels map[string]string, whereClauseItems []v3.FilterItem, groupByItems []v3.AttributeKey, keys map[string]v3.AttributeKey) []v3.FilterItem {
filterItems := make([]v3.FilterItem, 0)

View File

@@ -5,7 +5,7 @@ import (
"github.com/SigNoz/signoz/pkg/types/ruletypes"
)
// TODO(srikanthccv): Fix the URL management.
// TODO(srikanthccv): Fix the URL management
type URLShareableTimeRange struct {
Start int64 `json:"start"`
End int64 `json:"end"`

View File

@@ -155,7 +155,7 @@ func (b *base) WithAdditional(a ...string) *base {
// Otherwise, it returns TypeInternal, the original error string
// and the error itself.
//
//nolint:staticcheck // ST1008: intentional return order matching struct field order (TCMEUA)
//lint:ignore ST1008 we want to return arguments in the 'TCMEUA' order of the struct
func Unwrapb(cause error) (typ, Code, string, error, string, []string) {
base, ok := cause.(*base)
if ok {

View File

@@ -16,7 +16,7 @@ var (
TypeLicenseUnavailable = typ{"license-unavailable"}
)
// Defines custom error types.
// Defines custom error types
type typ struct{ s string }
func (t typ) String() string {

View File

@@ -115,7 +115,7 @@ func (f *flagger) Boolean(ctx context.Context, flag featuretypes.Name, evalCtx f
for _, client := range f.clients {
value, err := client.BooleanValue(ctx, flag.String(), defaultValue, evalCtx.Ctx())
if err != nil {
f.settings.Logger().ErrorContext(ctx, "failed to get value from client", errors.Attr(err), slog.Any("flag", flag), slog.String("client", client.Metadata().Domain()))
f.settings.Logger().ErrorContext(ctx, "failed to get value from client", errors.Attr(err), slog.Any("flag", flag), slog.String("client", client.Metadata().Name()))
continue
}
@@ -148,7 +148,7 @@ func (f *flagger) String(ctx context.Context, flag featuretypes.Name, evalCtx fe
for _, client := range f.clients {
value, err := client.StringValue(ctx, flag.String(), defaultValue, evalCtx.Ctx())
if err != nil {
f.settings.Logger().WarnContext(ctx, "failed to get value from client", errors.Attr(err), slog.Any("flag", flag), slog.String("client", client.Metadata().Domain()))
f.settings.Logger().WarnContext(ctx, "failed to get value from client", errors.Attr(err), slog.Any("flag", flag), slog.String("client", client.Metadata().Name()))
continue
}
@@ -181,7 +181,7 @@ func (f *flagger) Float(ctx context.Context, flag featuretypes.Name, evalCtx fea
for _, client := range f.clients {
value, err := client.FloatValue(ctx, flag.String(), defaultValue, evalCtx.Ctx())
if err != nil {
f.settings.Logger().WarnContext(ctx, "failed to get value from client", errors.Attr(err), slog.Any("flag", flag), slog.String("client", client.Metadata().Domain()))
f.settings.Logger().WarnContext(ctx, "failed to get value from client", errors.Attr(err), slog.Any("flag", flag), slog.String("client", client.Metadata().Name()))
continue
}
@@ -214,7 +214,7 @@ func (f *flagger) Int(ctx context.Context, flag featuretypes.Name, evalCtx featu
for _, client := range f.clients {
value, err := client.IntValue(ctx, flag.String(), defaultValue, evalCtx.Ctx())
if err != nil {
f.settings.Logger().WarnContext(ctx, "failed to get value from client", errors.Attr(err), slog.Any("flag", flag), slog.String("client", client.Metadata().Domain()))
f.settings.Logger().WarnContext(ctx, "failed to get value from client", errors.Attr(err), slog.Any("flag", flag), slog.String("client", client.Metadata().Name()))
continue
}
@@ -247,7 +247,7 @@ func (f *flagger) Object(ctx context.Context, flag featuretypes.Name, evalCtx fe
for _, client := range f.clients {
value, err := client.ObjectValue(ctx, flag.String(), defaultValue, evalCtx.Ctx())
if err != nil {
f.settings.Logger().WarnContext(ctx, "failed to get value from client", errors.Attr(err), slog.Any("flag", flag), slog.String("client", client.Metadata().Domain()))
f.settings.Logger().WarnContext(ctx, "failed to get value from client", errors.Attr(err), slog.Any("flag", flag), slog.String("client", client.Metadata().Name()))
continue
}

View File

@@ -17,7 +17,7 @@ type OpenAPIExample struct {
Value any
}
// Def is the definition of an OpenAPI operation.
// Def is the definition of an OpenAPI operation
type OpenAPIDef struct {
ID string
Tags []string
@@ -40,7 +40,7 @@ type OpenAPISecurityScheme struct {
Scopes []string
}
// OpenAPICollector is a collector for OpenAPI operations.
// Collector is a collector for OpenAPI operations
type OpenAPICollector struct {
collector *openapi.Collector
}

View File

@@ -6,15 +6,15 @@ const (
pkgname string = "go.signoz.io/pkg/http/middleware"
)
// Wrapper is an interface implemented by all middlewares.
// Wrapper is an interface implemented by all middlewares
type Wrapper interface {
Wrap(http.Handler) http.Handler
}
// WrapperFunc is to Wrapper as http.HandlerFunc is to http.Handler.
// WrapperFunc is to Wrapper as http.HandlerFunc is to http.Handler
type WrapperFunc func(http.Handler) http.Handler
// WrapperFunc implements Wrapper.
// WrapperFunc implements Wrapper
func (m WrapperFunc) Wrap(next http.Handler) http.Handler {
return m(next)
}

View File

@@ -46,7 +46,7 @@ type nonFlushingBadResponseLoggingWriter struct {
writeError error // The error returned when downstream Write() fails.
}
// Extends nonFlushingBadResponseLoggingWriter that implements http.Flusher.
// Extends nonFlushingBadResponseLoggingWriter that implements http.Flusher
type flushingBadResponseLoggingWriter struct {
nonFlushingBadResponseLoggingWriter
f http.Flusher

View File

@@ -5,7 +5,7 @@ var (
StatusError = status{"error"}
)
// Defines custom error types.
// Defines custom error types
type status struct{ s string }
func (s status) String() string {

View File

@@ -15,21 +15,17 @@ import (
type Instrumentation interface {
// Logger returns the Slog logger.
Logger() *slog.Logger
// MeterProvider returns the OpenTelemetry meter provider.
MeterProvider() sdkmetric.MeterProvider
// TracerProvider returns the OpenTelemetry tracer provider.
TracerProvider() sdktrace.TracerProvider
// PrometheusRegisterer returns the Prometheus registerer.
PrometheusRegisterer() prometheus.Registerer
// ToProviderSettings converts instrumentation to provider settings.
ToProviderSettings() factory.ProviderSettings
}
// ZapToSlogConverter defines conversion functions required for using zap interface with underlying slog provider.
// conversion functions required for using zap interface with underlying slog provider
type ZapToSlogConverter interface {
FieldsToAttributes(fields []zap.Field) []any
}

View File

@@ -5,10 +5,10 @@ import (
"log/slog"
)
// LogHandlerFunc is to LogHandler as http.HandlerFunc is to http.Handler.
// LogHandlerFunc is to LogHandler as http.HandlerFunc is to http.Handler
type LogHandlerFunc func(ctx context.Context, r slog.Record) error
// LogHandlerFunc implements LogHandler.
// LogHandlerFunc implements LogHandler
func (m LogHandlerFunc) Handle(ctx context.Context, r slog.Record) error {
return m(ctx, r)
}
@@ -17,7 +17,7 @@ type LogHandler interface {
Handle(ctx context.Context, r slog.Record) error
}
// Wrapper is an interface implemented by all log handlers.
// Wrapper is an interface implemented by all log handlers
type Wrapper interface {
Wrap(LogHandler) LogHandler
}

View File

@@ -22,7 +22,7 @@ import (
)
// readerWithServer wraps a metric reader with an HTTP server for proper shutdown
// This mirrors the upstream contrib/config implementation.
// This mirrors the upstream contrib/config implementation
type readerWithServer struct {
sdkmetric.Reader
server *http.Server
@@ -36,7 +36,7 @@ func (rws readerWithServer) Shutdown(ctx context.Context) error {
}
// prometheusReaderWithCustomRegistry creates a Prometheus metric reader using a custom registry
// This is based on the upstream contrib/config implementation but allows passing a custom registry.
// This is based on the upstream contrib/config implementation but allows passing a custom registry
func prometheusReaderWithCustomRegistry(ctx context.Context, prometheusConfig *contribsdkconfig.Prometheus, customRegistry *prometheus.Registry) (sdkmetric.Reader, error) {
var opts []otelprom.Option
if prometheusConfig.Host == nil {
@@ -49,10 +49,10 @@ func prometheusReaderWithCustomRegistry(ctx context.Context, prometheusConfig *c
opts = append(opts, otelprom.WithoutScopeInfo())
}
if prometheusConfig.WithoutTypeSuffix != nil && *prometheusConfig.WithoutTypeSuffix {
opts = append(opts, otelprom.WithoutCounterSuffixes()) //nolint:staticcheck // SA1019: WithTranslationStrategy does not provide equivalent per-option granularity
opts = append(opts, otelprom.WithoutCounterSuffixes())
}
if prometheusConfig.WithoutUnits != nil && *prometheusConfig.WithoutUnits {
opts = append(opts, otelprom.WithoutUnits()) //nolint:staticcheck // SA1019: WithTranslationStrategy does not provide equivalent per-option granularity
opts = append(opts, otelprom.WithoutUnits())
}
if prometheusConfig.WithResourceConstantLabels != nil {
if prometheusConfig.WithResourceConstantLabels.Included != nil {
@@ -109,11 +109,11 @@ func prometheusReaderWithCustomRegistry(ctx context.Context, prometheusConfig *c
type shutdownFunc func(context.Context) error
// noopShutdown is a no-op shutdown function.
// noopShutdown is a no-op shutdown function
func noopShutdown(context.Context) error { return nil }
// meterProviderWithCustomRegistry creates a meter provider using contrib config approach
// but with custom Prometheus registry injection.
// but with custom Prometheus registry injection
func meterProviderWithCustomRegistry(ctx context.Context, meterProviderConfig *contribsdkconfig.MeterProvider, res *resource.Resource, customRegistry *prometheus.Registry) (metric.MeterProvider, shutdownFunc, error) {
if meterProviderConfig == nil {
return noop.NewMeterProvider(), noopShutdown, nil
@@ -140,7 +140,7 @@ func meterProviderWithCustomRegistry(ctx context.Context, meterProviderConfig *c
return mp, mp.Shutdown, nil
}
// metricReaderWithCustomRegistry creates metric readers with custom Prometheus registry support.
// metricReaderWithCustomRegistry creates metric readers with custom Prometheus registry support
func metricReaderWithCustomRegistry(ctx context.Context, r contribsdkconfig.MetricReader, customRegistry *prometheus.Registry) (sdkmetric.Reader, error) {
if r.Periodic != nil && r.Pull != nil {
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "must not specify multiple metric reader type")
@@ -152,7 +152,7 @@ func metricReaderWithCustomRegistry(ctx context.Context, r contribsdkconfig.Metr
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "no valid metric reader")
}
// pullReaderWithCustomRegistry creates pull readers with custom Prometheus registry support.
// pullReaderWithCustomRegistry creates pull readers with custom Prometheus registry support
func pullReaderWithCustomRegistry(ctx context.Context, exporter contribsdkconfig.MetricExporter, customRegistry *prometheus.Registry) (sdkmetric.Reader, error) {
if exporter.Prometheus != nil {
return prometheusReaderWithCustomRegistry(ctx, exporter.Prometheus, customRegistry)

View File

@@ -63,7 +63,7 @@ func (module *module) Get(ctx context.Context, orgID string, services []string)
func (module *module) Set(ctx context.Context, orgID string, apdexSettings *apdextypes.Settings) error {
apdexSettings.OrgID = orgID
apdexSettings.ID = valuer.GenerateUUID()
apdexSettings.Identifiable.ID = valuer.GenerateUUID()
_, err := module.
sqlstore.

View File

@@ -14,6 +14,7 @@ import (
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -210,7 +211,7 @@ func (module *module) MustGetManagedRoleTransactions() map[string][]*authtypes.T
return nil
}
// CreatePublic is not supported.
// not supported
func (module *module) CreatePublic(ctx context.Context, orgID valuer.UUID, publicDashboard *dashboardtypes.PublicDashboard) error {
return errors.Newf(errors.TypeUnsupported, dashboardtypes.ErrCodePublicDashboardUnsupported, "not implemented")
}
@@ -223,7 +224,7 @@ func (module *module) GetDashboardByPublicID(_ context.Context, _ valuer.UUID) (
return nil, errors.Newf(errors.TypeUnsupported, dashboardtypes.ErrCodePublicDashboardUnsupported, "not implemented")
}
func (module *module) GetPublicWidgetQueryRange(context.Context, valuer.UUID, uint64, uint64, uint64) (*qbtypes.QueryRangeResponse, error) {
func (module *module) GetPublicWidgetQueryRange(context.Context, valuer.UUID, uint64, uint64, uint64) (*querybuildertypesv5.QueryRangeResponse, error) {
return nil, errors.Newf(errors.TypeUnsupported, dashboardtypes.ErrCodePublicDashboardUnsupported, "not implemented")
}
@@ -239,7 +240,7 @@ func (module *module) DeletePublic(_ context.Context, _ valuer.UUID, _ valuer.UU
return errors.Newf(errors.TypeUnsupported, dashboardtypes.ErrCodePublicDashboardUnsupported, "not implemented")
}
// checkBuilderQueriesForMetricNames checks builder.queryData[] for aggregations[].metricName.
// checkBuilderQueriesForMetricNames checks builder.queryData[] for aggregations[].metricName
func (module *module) checkBuilderQueriesForMetricNames(query map[string]interface{}, metricNames []string, foundMetrics map[string]bool) {
builder, ok := query["builder"].(map[string]interface{})
if !ok {
@@ -286,7 +287,7 @@ func (module *module) checkBuilderQueriesForMetricNames(query map[string]interfa
}
}
// checkClickHouseQueriesForMetricNames checks clickhouse_sql[] array for metric names in query strings.
// checkClickHouseQueriesForMetricNames checks clickhouse_sql[] array for metric names in query strings
func (module *module) checkClickHouseQueriesForMetricNames(ctx context.Context, query map[string]interface{}, metricNames []string, foundMetrics map[string]bool) {
clickhouseSQL, ok := query["clickhouse_sql"].([]interface{})
if !ok {
@@ -321,7 +322,7 @@ func (module *module) checkClickHouseQueriesForMetricNames(ctx context.Context,
}
}
// checkPromQLQueriesForMetricNames checks promql[] array for metric names in query strings.
// checkPromQLQueriesForMetricNames checks promql[] array for metric names in query strings
func (module *module) checkPromQLQueriesForMetricNames(ctx context.Context, query map[string]interface{}, metricNames []string, foundMetrics map[string]bool) {
promQL, ok := query["promql"].([]interface{})
if !ok {

View File

@@ -300,41 +300,6 @@ func (h *handler) GetMetricAttributes(rw http.ResponseWriter, req *http.Request)
render.Success(rw, http.StatusOK, out)
}
func (h *handler) InspectMetrics(rw http.ResponseWriter, req *http.Request) {
claims, err := authtypes.ClaimsFromContext(req.Context())
if err != nil {
render.Error(rw, err)
return
}
var in metricsexplorertypes.InspectMetricsRequest
if err := binding.JSON.BindBody(req.Body, &in); err != nil {
render.Error(rw, err)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
out, err := h.module.InspectMetrics(req.Context(), orgID, &in)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, out)
}
func (h *handler) GetOnboardingStatus(rw http.ResponseWriter, req *http.Request) {
hasMetrics, err := h.module.HasNonSigNozMetrics(req.Context())
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, &metricsexplorertypes.MetricsOnboardingResponse{
HasMetrics: hasMetrics,
})
}
func (h *handler) checkMetricExists(ctx context.Context, orgID valuer.UUID, metricName string) error {
exists, err := h.module.CheckMetricExists(ctx, orgID, metricName)
if err != nil {

View File

@@ -9,7 +9,7 @@ import (
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
)
// used for mapping the sqlColumns via orderBy.
// used for mapping the sqlColumns via orderBy
const (
sqlColumnTimeSeries = "timeseries"
sqlColumnSamples = "samples"

View File

@@ -3,7 +3,6 @@ package implmetricsexplorer
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"log/slog"
"strings"
@@ -60,7 +59,7 @@ func NewModule(ts telemetrystore.TelemetryStore, telemetryMetadataStore telemetr
}
}
// TODO(srikanthccv): use metadata store to fetch metric metadata.
// TODO(srikanthccv): use metadata store to fetch metric metadata
func (m *module) ListMetrics(ctx context.Context, orgID valuer.UUID, params *metricsexplorertypes.ListMetricsParams) (*metricsexplorertypes.ListMetricsResponse, error) {
ctx = m.withMetricsExplorerContext(ctx, "ListMetrics")
@@ -503,157 +502,6 @@ func (m *module) CheckMetricExists(ctx context.Context, orgID valuer.UUID, metri
return exists, nil
}
func (m *module) HasNonSigNozMetrics(ctx context.Context) (bool, error) {
ctx = m.withMetricsExplorerContext(ctx, "HasNonSigNozMetrics")
sb := sqlbuilder.NewSelectBuilder()
sb.Select("count(*) > 0")
sb.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, telemetrymetrics.TimeseriesV41weekTableName))
sb.Where("metric_name NOT LIKE 'signoz_%'")
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
db := m.telemetryStore.ClickhouseDB()
var hasMetrics bool
valueCtx := ctxtypes.SetClickhouseMaxThreads(ctx, m.config.TelemetryStore.Threads)
err := db.QueryRow(valueCtx, query, args...).Scan(&hasMetrics)
if err != nil {
return false, errors.WrapInternalf(err, errors.CodeInternal, "failed to check for non-signoz metrics")
}
return hasMetrics, nil
}
func (m *module) InspectMetrics(
ctx context.Context,
orgID valuer.UUID,
req *metricsexplorertypes.InspectMetricsRequest,
) (*metricsexplorertypes.InspectMetricsResponse, error) {
ctx = m.withMetricsExplorerContext(ctx, "InspectMetrics")
if err := req.Validate(); err != nil {
return nil, err
}
start := uint64(req.Start)
end := uint64(req.End)
filterWhereClause, err := m.buildFilterClause(ctx, req.Filter, req.Start, req.End)
if err != nil {
return nil, err
}
tsStart, _, tsTable, _ := telemetrymetrics.WhichTSTableToUse(start, end, nil)
tsSb := sqlbuilder.NewSelectBuilder()
tsSb.Select("fingerprint", "labels")
tsSb.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, tsTable))
tsSb.Where(
tsSb.E("metric_name", req.MetricName),
tsSb.GE("unix_milli", tsStart),
tsSb.LE("unix_milli", end),
)
if filterWhereClause != nil {
tsSb.AddWhereClause(sqlbuilder.CopyWhereClause(filterWhereClause))
}
tsSb.GroupBy("fingerprint", "labels")
tsSb.Limit(50)
tsQuery, tsArgs := tsSb.BuildWithFlavor(sqlbuilder.ClickHouse)
db := m.telemetryStore.ClickhouseDB()
valueCtx := ctxtypes.SetClickhouseMaxThreads(ctx, m.config.TelemetryStore.Threads)
tsRows, err := db.Query(valueCtx, tsQuery, tsArgs...)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to query time series for inspect")
}
defer tsRows.Close()
type tsInfo struct {
fingerprint uint64
labels map[string]string
}
var tsList []tsInfo
var fingerprints []any
for tsRows.Next() {
var fp uint64
var labelsStr string
if err := tsRows.Scan(&fp, &labelsStr); err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to scan time series row")
}
parsedLabels := make(map[string]string)
if err := json.Unmarshal([]byte(labelsStr), &parsedLabels); err != nil {
parsedLabels = map[string]string{}
}
tsList = append(tsList, tsInfo{fingerprint: fp, labels: parsedLabels})
fingerprints = append(fingerprints, fp)
}
if err := tsRows.Err(); err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to iterate time series rows")
}
if len(fingerprints) == 0 {
return &metricsexplorertypes.InspectMetricsResponse{Series: []*qbtypes.TimeSeries{}}, nil
}
samplesSb := sqlbuilder.NewSelectBuilder()
samplesSb.Select("fingerprint", "unix_milli", "value")
samplesSb.From(fmt.Sprintf("%s.%s", telemetrymetrics.DBName, telemetrymetrics.SamplesV4TableName))
samplesSb.Where(
samplesSb.In("fingerprint", fingerprints...),
samplesSb.E("metric_name", req.MetricName),
samplesSb.GE("unix_milli", int64(start)),
samplesSb.LE("unix_milli", int64(end)),
)
samplesSb.OrderBy("fingerprint", "unix_milli")
samplesQuery, samplesArgs := samplesSb.BuildWithFlavor(sqlbuilder.ClickHouse)
samplesRows, err := db.Query(valueCtx, samplesQuery, samplesArgs...)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to query samples for inspect")
}
defer samplesRows.Close()
valuesMap := make(map[uint64][]*qbtypes.TimeSeriesValue)
for samplesRows.Next() {
var fp uint64
var unixMilli int64
var value float64
if err := samplesRows.Scan(&fp, &unixMilli, &value); err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to scan samples row")
}
valuesMap[fp] = append(valuesMap[fp], &qbtypes.TimeSeriesValue{
Timestamp: unixMilli,
Value: value,
})
}
if err := samplesRows.Err(); err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to iterate samples rows")
}
series := make([]*qbtypes.TimeSeries, 0, len(tsList))
for _, ts := range tsList {
labels := make([]*qbtypes.Label, 0, len(ts.labels))
for k, v := range ts.labels {
labels = append(labels, &qbtypes.Label{
Key: telemetrytypes.TelemetryFieldKey{Name: k},
Value: v,
})
}
values := valuesMap[ts.fingerprint]
if values == nil {
values = []*qbtypes.TimeSeriesValue{}
}
series = append(series, &qbtypes.TimeSeries{
Labels: labels,
Values: values,
})
}
return &metricsexplorertypes.InspectMetricsResponse{Series: series}, nil
}
func (m *module) fetchMetadataFromCache(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string]*metricsexplorertypes.MetricMetadata, []string) {
hits := make(map[string]*metricsexplorertypes.MetricMetadata)
misses := make([]string, 0)
@@ -663,7 +511,7 @@ func (m *module) fetchMetadataFromCache(ctx context.Context, orgID valuer.UUID,
if err := m.cache.Get(ctx, orgID, cacheKey, &cachedMetadata); err == nil {
hits[metricName] = &cachedMetadata
} else {
m.logger.WarnContext(ctx, "cache miss for metric metadata", slog.String("metric_name", metricName))
m.logger.WarnContext(ctx, "cache miss for metric metadata", slog.String("metric_name", metricName), errors.Attr(err))
misses = append(misses, metricName)
}
}

View File

@@ -19,8 +19,6 @@ type Handler interface {
GetMetricAlerts(http.ResponseWriter, *http.Request)
GetMetricDashboards(http.ResponseWriter, *http.Request)
GetMetricHighlights(http.ResponseWriter, *http.Request)
GetOnboardingStatus(http.ResponseWriter, *http.Request)
InspectMetrics(http.ResponseWriter, *http.Request)
}
// Module represents the metrics module interface.
@@ -35,6 +33,4 @@ type Module interface {
GetMetricDashboards(ctx context.Context, orgID valuer.UUID, metricName string) (*metricsexplorertypes.MetricDashboardsResponse, error)
GetMetricHighlights(ctx context.Context, orgID valuer.UUID, metricName string) (*metricsexplorertypes.MetricHighlightsResponse, error)
GetMetricAttributes(ctx context.Context, orgID valuer.UUID, req *metricsexplorertypes.MetricAttributesRequest) (*metricsexplorertypes.MetricAttributesResponse, error)
HasNonSigNozMetrics(ctx context.Context) (bool, error)
InspectMetrics(ctx context.Context, orgID valuer.UUID, req *metricsexplorertypes.InspectMetricsRequest) (*metricsexplorertypes.InspectMetricsResponse, error)
}

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