Compare commits

..

1 Commits

Author SHA1 Message Date
Ishan Uniyal
5320138eb9 fix: added event propogation on enter press 2026-03-13 13:01:21 +05:30
134 changed files with 1591 additions and 5256 deletions

15
.github/CODEOWNERS vendored
View File

@@ -1,6 +1,8 @@
# CODEOWNERS info: https://help.github.com/en/articles/about-code-owners
# Owners are automatically requested for review for PRs that changes code that they own.
# Owners are automatically requested for review for PRs that changes code
# that they own.
/frontend/ @SigNoz/frontend-maintainers
@@ -9,10 +11,8 @@
/frontend/src/container/OnboardingV2Container/onboarding-configs/onboarding-config-with-links.json @makeavish
/frontend/src/container/OnboardingV2Container/AddDataSource/AddDataSource.tsx @makeavish
# CI
/deploy/ @therealpandey
.github @therealpandey
go.mod @therealpandey
/deploy/ @SigNoz/devops
.github @SigNoz/devops
# Scaffold Owners
@@ -127,15 +127,12 @@ go.mod @therealpandey
/frontend/src/pages/DashboardsListPage/ @SigNoz/pulse-frontend
/frontend/src/container/ListOfDashboard/ @SigNoz/pulse-frontend
# Dashboard Widget Page
/frontend/src/pages/DashboardWidget/ @SigNoz/pulse-frontend
/frontend/src/container/NewWidget/ @SigNoz/pulse-frontend
## Dashboard Page
/frontend/src/pages/DashboardPage/ @SigNoz/pulse-frontend
/frontend/src/container/DashboardContainer/ @SigNoz/pulse-frontend
/frontend/src/container/GridCardLayout/ @SigNoz/pulse-frontend
/frontend/src/container/NewWidget/ @SigNoz/pulse-frontend
## Public Dashboard Page

View File

@@ -1,60 +0,0 @@
name: mergequeueci
on:
pull_request:
types:
- dequeued
jobs:
notify:
runs-on: ubuntu-latest
if: github.event.pull_request.merged == false
steps:
- name: alert
uses: slackapi/slack-github-action@v2.1.1
with:
webhook: ${{ secrets.SLACK_MERGE_QUEUE_WEBHOOK }}
webhook-type: incoming-webhook
payload: |
{
"text": ":x: PR removed from merge queue",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": ":x: PR Removed from Merge Queue"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*<${{ github.event.pull_request.html_url }}|PR #${{ github.event.pull_request.number }}: ${{ github.event.pull_request.title }}>*"
}
},
{
"type": "divider"
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": "*Author*\n@${{ github.event.pull_request.user.login }}"
}
]
}
]
}
- name: comment
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
PR_URL: ${{ github.event.pull_request.html_url }}
run: |
gh api repos/${{ github.repository }}/issues/$PR_NUMBER/comments \
-f body="> :x: **PR removed from merge queue**
>
> @$PR_AUTHOR your PR was removed from the merge queue. Fix the issue and re-queue when ready."

View File

@@ -6,6 +6,7 @@ import (
"github.com/SigNoz/signoz/pkg/version"
"github.com/spf13/cobra"
"go.uber.org/zap" //nolint:depguard
)
var RootCmd = &cobra.Command{
@@ -18,6 +19,12 @@ var RootCmd = &cobra.Command{
}
func Execute(logger *slog.Logger) {
zapLogger := newZapLogger()
zap.ReplaceGlobals(zapLogger)
defer func() {
_ = zapLogger.Sync()
}()
err := RootCmd.Execute()
if err != nil {
logger.ErrorContext(RootCmd.Context(), "error running command", "error", err)

110
cmd/zap.go Normal file
View File

@@ -0,0 +1,110 @@
package cmd
import (
"context"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"go.uber.org/zap" //nolint:depguard
"go.uber.org/zap/zapcore" //nolint:depguard
)
// Deprecated: Use `NewLogger` from `pkg/instrumentation` instead.
func newZapLogger() *zap.Logger {
config := zap.NewProductionConfig()
config.EncoderConfig.TimeKey = "timestamp"
config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
// Extract sampling config before building the logger.
// We need to disable sampling in the config and apply it manually later
// to ensure correct core ordering. See filteringCore documentation for details.
samplerConfig := config.Sampling
config.Sampling = nil
logger, _ := config.Build()
// Wrap with custom core wrapping to filter certain log entries.
// The order of wrapping is important:
// 1. First wrap with filteringCore
// 2. Then wrap with sampler
//
// This creates the call chain: sampler -> filteringCore -> ioCore
//
// During logging:
// - sampler.Check decides whether to sample the log entry
// - If sampled, filteringCore.Check is called
// - filteringCore adds itself to CheckedEntry.cores
// - All cores in CheckedEntry.cores have their Write method called
// - filteringCore.Write can now filter the entry before passing to ioCore
//
// If we didn't disable the sampler above, filteringCore would have wrapped
// sampler. By calling sampler.Check we would have allowed it to call
// ioCore.Check that adds itself to CheckedEntry.cores. Then ioCore.Write
// would have bypassed our checks, making filtering impossible.
return logger.WithOptions(zap.WrapCore(func(core zapcore.Core) zapcore.Core {
core = &filteringCore{core}
if samplerConfig != nil {
core = zapcore.NewSamplerWithOptions(
core,
time.Second,
samplerConfig.Initial,
samplerConfig.Thereafter,
)
}
return core
}))
}
// filteringCore wraps a zapcore.Core to filter out log entries based on a
// custom logic.
//
// Note: This core must be positioned before the sampler in the core chain
// to ensure Write is called. See newZapLogger for ordering details.
type filteringCore struct {
zapcore.Core
}
// filter determines whether a log entry should be written based on its fields.
// Returns false if the entry should be suppressed, true otherwise.
//
// Current filters:
// - context.Canceled: These are expected errors from cancelled operations,
// and create noise in logs.
func (c *filteringCore) filter(fields []zapcore.Field) bool {
for _, field := range fields {
if field.Type == zapcore.ErrorType {
if loggedErr, ok := field.Interface.(error); ok {
// Suppress logs containing context.Canceled errors
if errors.Is(loggedErr, context.Canceled) {
return false
}
}
}
}
return true
}
// With implements zapcore.Core.With
// It returns a new copy with the added context.
func (c *filteringCore) With(fields []zapcore.Field) zapcore.Core {
return &filteringCore{c.Core.With(fields)}
}
// Check implements zapcore.Core.Check.
// It adds this core to the CheckedEntry if the log level is enabled,
// ensuring that Write will be called for this entry.
func (c *filteringCore) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
if c.Enabled(ent.Level) {
return ce.AddCore(ent, c)
}
return ce
}
// Write implements zapcore.Core.Write.
// It filters log entries based on their fields before delegating to the wrapped core.
func (c *filteringCore) Write(ent zapcore.Entry, fields []zapcore.Field) error {
if !c.filter(fields) {
return nil
}
return c.Core.Write(ent, fields)
}

View File

@@ -2,7 +2,6 @@ package anomaly
import (
"context"
"log/slog"
"math"
"time"
@@ -14,6 +13,7 @@ import (
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"go.uber.org/zap"
)
var (
@@ -67,7 +67,7 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, orgID valuer.UUID
instrumentationtypes.CodeNamespace: "anomaly",
instrumentationtypes.CodeFunctionName: "getResults",
})
slog.InfoContext(ctx, "fetching results for current period", "current_period_query", params.CurrentPeriodQuery)
zap.L().Info("fetching results for current period", zap.Any("currentPeriodQuery", params.CurrentPeriodQuery))
currentPeriodResults, _, err := p.querierV2.QueryRange(ctx, orgID, params.CurrentPeriodQuery)
if err != nil {
return nil, err
@@ -78,7 +78,7 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, orgID valuer.UUID
return nil, err
}
slog.InfoContext(ctx, "fetching results for past period", "past_period_query", params.PastPeriodQuery)
zap.L().Info("fetching results for past period", zap.Any("pastPeriodQuery", params.PastPeriodQuery))
pastPeriodResults, _, err := p.querierV2.QueryRange(ctx, orgID, params.PastPeriodQuery)
if err != nil {
return nil, err
@@ -89,7 +89,7 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, orgID valuer.UUID
return nil, err
}
slog.InfoContext(ctx, "fetching results for current season", "current_season_query", params.CurrentSeasonQuery)
zap.L().Info("fetching results for current season", zap.Any("currentSeasonQuery", params.CurrentSeasonQuery))
currentSeasonResults, _, err := p.querierV2.QueryRange(ctx, orgID, params.CurrentSeasonQuery)
if err != nil {
return nil, err
@@ -100,7 +100,7 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, orgID valuer.UUID
return nil, err
}
slog.InfoContext(ctx, "fetching results for past season", "past_season_query", params.PastSeasonQuery)
zap.L().Info("fetching results for past season", zap.Any("pastSeasonQuery", params.PastSeasonQuery))
pastSeasonResults, _, err := p.querierV2.QueryRange(ctx, orgID, params.PastSeasonQuery)
if err != nil {
return nil, err
@@ -111,7 +111,7 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, orgID valuer.UUID
return nil, err
}
slog.InfoContext(ctx, "fetching results for past 2 season", "past_2_season_query", params.Past2SeasonQuery)
zap.L().Info("fetching results for past 2 season", zap.Any("past2SeasonQuery", params.Past2SeasonQuery))
past2SeasonResults, _, err := p.querierV2.QueryRange(ctx, orgID, params.Past2SeasonQuery)
if err != nil {
return nil, err
@@ -122,7 +122,7 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, orgID valuer.UUID
return nil, err
}
slog.InfoContext(ctx, "fetching results for past 3 season", "past_3_season_query", params.Past3SeasonQuery)
zap.L().Info("fetching results for past 3 season", zap.Any("past3SeasonQuery", params.Past3SeasonQuery))
past3SeasonResults, _, err := p.querierV2.QueryRange(ctx, orgID, params.Past3SeasonQuery)
if err != nil {
return nil, err
@@ -235,17 +235,17 @@ func (p *BaseSeasonalProvider) getPredictedSeries(
if predictedValue < 0 {
// this should not happen (except when the data has extreme outliers)
// we will use the moving avg of the previous period series in this case
slog.Warn("predicted value is less than 0", "predicted_value", predictedValue, "labels", series.Labels)
zap.L().Warn("predictedValue is less than 0", zap.Float64("predictedValue", predictedValue), zap.Any("labels", series.Labels))
predictedValue = p.getMovingAvg(prevSeries, movingAvgWindowSize, idx)
}
slog.Debug("predicted series",
"moving_avg", movingAvg,
"avg", avg,
"mean", mean,
"labels", series.Labels,
"predicted_value", predictedValue,
"curr", curr.Value,
zap.L().Debug("predictedSeries",
zap.Float64("movingAvg", movingAvg),
zap.Float64("avg", avg),
zap.Float64("mean", mean),
zap.Any("labels", series.Labels),
zap.Float64("predictedValue", predictedValue),
zap.Float64("curr", curr.Value),
)
predictedSeries.Points = append(predictedSeries.Points, v3.Point{
Timestamp: curr.Timestamp,
@@ -418,7 +418,7 @@ func (p *BaseSeasonalProvider) getAnomalies(ctx context.Context, orgID valuer.UU
for _, series := range result.Series {
stdDev := p.getStdDev(series)
slog.InfoContext(ctx, "computed standard deviation", "std_dev", stdDev, "labels", series.Labels)
zap.L().Info("stdDev", zap.Float64("stdDev", stdDev), zap.Any("labels", series.Labels))
pastPeriodSeries := p.getMatchingSeries(pastPeriodResult, series)
currentSeasonSeries := p.getMatchingSeries(currentSeasonResult, series)
@@ -431,7 +431,7 @@ func (p *BaseSeasonalProvider) getAnomalies(ctx context.Context, orgID valuer.UU
pastSeasonSeriesAvg := p.getAvg(pastSeasonSeries)
past2SeasonSeriesAvg := p.getAvg(past2SeasonSeries)
past3SeasonSeriesAvg := p.getAvg(past3SeasonSeries)
slog.InfoContext(ctx, "computed averages", "prev_series_avg", prevSeriesAvg, "current_season_series_avg", currentSeasonSeriesAvg, "past_season_series_avg", pastSeasonSeriesAvg, "past_2_season_series_avg", past2SeasonSeriesAvg, "past_3_season_series_avg", past3SeasonSeriesAvg, "labels", series.Labels)
zap.L().Info("getAvg", zap.Float64("prevSeriesAvg", prevSeriesAvg), zap.Float64("currentSeasonSeriesAvg", currentSeasonSeriesAvg), zap.Float64("pastSeasonSeriesAvg", pastSeasonSeriesAvg), zap.Float64("past2SeasonSeriesAvg", past2SeasonSeriesAvg), zap.Float64("past3SeasonSeriesAvg", past3SeasonSeriesAvg), zap.Any("labels", series.Labels))
predictedSeries := p.getPredictedSeries(
series,

View File

@@ -18,7 +18,7 @@ import (
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
"log/slog"
"go.uber.org/zap"
)
type CloudIntegrationConnectionParamsResponse struct {
@@ -71,7 +71,7 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
// Return the API Key (PAT) even if the rest of the params can not be deduced.
// Params not returned from here will be requested from the user via form inputs.
// This enables gracefully degraded but working experience even for non-cloud deployments.
slog.InfoContext(r.Context(), "ingestion params and signoz api url can not be deduced since no license was found")
zap.L().Info("ingestion params and signoz api url can not be deduced since no license was found")
ah.Respond(w, result)
return
}
@@ -103,7 +103,7 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
result.IngestionKey = ingestionKey
} else {
slog.InfoContext(r.Context(), "ingestion key can't be deduced since no gateway url has been configured")
zap.L().Info("ingestion key can't be deduced since no gateway url has been configured")
}
ah.Respond(w, result)
@@ -138,8 +138,9 @@ func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId
}
}
slog.InfoContext(ctx, "no PAT found for cloud integration, creating a new one",
"cloud_provider", cloudProvider,
zap.L().Info(
"no PAT found for cloud integration, creating a new one",
zap.String("cloudProvider", cloudProvider),
)
newPAT, err := types.NewStorableAPIKey(
@@ -286,8 +287,9 @@ func getOrCreateCloudProviderIngestionKey(
}
}
slog.InfoContext(ctx, "no existing ingestion key found for cloud integration, creating a new one",
"cloud_provider", cloudProvider,
zap.L().Info(
"no existing ingestion key found for cloud integration, creating a new one",
zap.String("cloudProvider", cloudProvider),
)
createKeyResult, apiErr := requestGateway[createIngestionKeyResponse](
ctx, gatewayUrl, licenseKey, "/v1/workspaces/me/keys",

View File

@@ -15,7 +15,7 @@ import (
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
"log/slog"
"go.uber.org/zap"
)
func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
@@ -35,23 +35,23 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
}
if constants.FetchFeatures == "true" {
slog.DebugContext(ctx, "fetching license")
zap.L().Debug("fetching license")
license, err := ah.Signoz.Licensing.GetActive(ctx, orgID)
if err != nil {
slog.ErrorContext(ctx, "failed to fetch license", "error", err)
zap.L().Error("failed to fetch license", zap.Error(err))
} else if license == nil {
slog.DebugContext(ctx, "no active license found")
zap.L().Debug("no active license found")
} else {
licenseKey := license.Key
slog.DebugContext(ctx, "fetching zeus features")
zap.L().Debug("fetching zeus features")
zeusFeatures, err := fetchZeusFeatures(constants.ZeusFeaturesURL, licenseKey)
if err == nil {
slog.DebugContext(ctx, "fetched zeus features", "features", zeusFeatures)
zap.L().Debug("fetched zeus features", zap.Any("features", zeusFeatures))
// merge featureSet and zeusFeatures in featureSet with higher priority to zeusFeatures
featureSet = MergeFeatureSets(zeusFeatures, featureSet)
} else {
slog.ErrorContext(ctx, "failed to fetch zeus features", "error", err)
zap.L().Error("failed to fetch zeus features", zap.Error(err))
}
}
}

View File

@@ -14,7 +14,7 @@ import (
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"log/slog"
"go.uber.org/zap"
)
func (aH *APIHandler) queryRangeV4(w http.ResponseWriter, r *http.Request) {
@@ -35,7 +35,7 @@ func (aH *APIHandler) queryRangeV4(w http.ResponseWriter, r *http.Request) {
queryRangeParams, apiErrorObj := baseapp.ParseQueryRangeParams(r)
if apiErrorObj != nil {
slog.ErrorContext(r.Context(), "error parsing metric query range params", "error", apiErrorObj.Err)
zap.L().Error("error parsing metric query range params", zap.Error(apiErrorObj.Err))
RespondError(w, apiErrorObj, nil)
return
}
@@ -44,7 +44,7 @@ func (aH *APIHandler) queryRangeV4(w http.ResponseWriter, r *http.Request) {
// add temporality for each metric
temporalityErr := aH.PopulateTemporality(r.Context(), orgID, queryRangeParams)
if temporalityErr != nil {
slog.ErrorContext(r.Context(), "error while adding temporality for metrics", "error", temporalityErr)
zap.L().Error("Error while adding temporality for metrics", zap.Error(temporalityErr))
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: temporalityErr}, nil)
return
}

View File

@@ -47,7 +47,7 @@ import (
baseint "github.com/SigNoz/signoz/pkg/query-service/interfaces"
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
"github.com/SigNoz/signoz/pkg/query-service/utils"
"log/slog"
"go.uber.org/zap"
)
// Server runs HTTP, Mux and a grpc server
@@ -83,7 +83,6 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
}
reader := clickhouseReader.NewReader(
signoz.Instrumentation.Logger(),
signoz.SQLStore,
signoz.TelemetryStore,
signoz.Prometheus,
@@ -279,7 +278,7 @@ func (s *Server) initListeners() error {
return err
}
slog.Info(fmt.Sprintf("Query server started listening on %s...", s.httpHostPort))
zap.L().Info(fmt.Sprintf("Query server started listening on %s...", s.httpHostPort))
return nil
}
@@ -299,31 +298,31 @@ func (s *Server) Start(ctx context.Context) error {
}
go func() {
slog.Info("Starting HTTP server", "port", httpPort, "addr", s.httpHostPort)
zap.L().Info("Starting HTTP server", zap.Int("port", httpPort), zap.String("addr", s.httpHostPort))
switch err := s.httpServer.Serve(s.httpConn); err {
case nil, http.ErrServerClosed, cmux.ErrListenerClosed:
// normal exit, nothing to do
default:
slog.Error("Could not start HTTP server", "error", err)
zap.L().Error("Could not start HTTP server", zap.Error(err))
}
s.unavailableChannel <- healthcheck.Unavailable
}()
go func() {
slog.Info("Starting pprof server", "addr", baseconst.DebugHttpPort)
zap.L().Info("Starting pprof server", zap.String("addr", baseconst.DebugHttpPort))
err = http.ListenAndServe(baseconst.DebugHttpPort, nil)
if err != nil {
slog.Error("Could not start pprof server", "error", err)
zap.L().Error("Could not start pprof server", zap.Error(err))
}
}()
go func() {
slog.Info("Starting OpAmp Websocket server", "addr", baseconst.OpAmpWsEndpoint)
zap.L().Info("Starting OpAmp Websocket server", zap.String("addr", baseconst.OpAmpWsEndpoint))
err := s.opampServer.Start(baseconst.OpAmpWsEndpoint)
if err != nil {
slog.Error("opamp ws server failed to start", "error", err)
zap.L().Error("opamp ws server failed to start", zap.Error(err))
s.unavailableChannel <- healthcheck.Unavailable
}
}()
@@ -359,9 +358,10 @@ func makeRulesManager(ch baseint.Reader, cache cache.Cache, alertmanager alertma
MetadataStore: metadataStore,
Prometheus: prometheus,
Context: context.Background(),
Logger: zap.L(),
Reader: ch,
Querier: querier,
Logger: providerSettings.Logger,
SLogger: providerSettings.Logger,
Cache: cache,
EvalDelay: baseconst.GetEvalDelay(),
PrepareTaskFunc: rules.PrepareTaskFunc,
@@ -380,7 +380,7 @@ func makeRulesManager(ch baseint.Reader, cache cache.Cache, alertmanager alertma
return nil, fmt.Errorf("rule manager error: %v", err)
}
slog.Info("rules manager is ready")
zap.L().Info("rules manager is ready")
return manager, nil
}

View File

@@ -2,7 +2,6 @@ package rules
import (
"context"
"log/slog"
"testing"
"time"
@@ -117,7 +116,7 @@ func TestAnomalyRule_NoData_AlertOnAbsent(t *testing.T) {
telemetryStore := telemetrystoretest.New(telemetrystore.Config{}, nil)
options := clickhouseReader.NewOptions("primaryNamespace")
reader := clickhouseReader.NewReader(slog.Default(), nil, telemetryStore, nil, "", time.Second, nil, nil, options)
reader := clickhouseReader.NewReader(nil, telemetryStore, nil, "", time.Second, nil, nil, options)
rule, err := NewAnomalyRule(
"test-anomaly-rule",
@@ -248,7 +247,7 @@ func TestAnomalyRule_NoData_AbsentFor(t *testing.T) {
telemetryStore := telemetrystoretest.New(telemetrystore.Config{}, nil)
options := clickhouseReader.NewOptions("primaryNamespace")
reader := clickhouseReader.NewReader(slog.Default(), nil, telemetryStore, nil, "", time.Second, nil, nil, options)
reader := clickhouseReader.NewReader(nil, telemetryStore, nil, "", time.Second, nil, nil, options)
rule, err := NewAnomalyRule("test-anomaly-rule", valuer.GenerateUUID(), &postableRule, reader, nil, logger, nil)
require.NoError(t, err)

View File

@@ -13,7 +13,7 @@ import (
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/google/uuid"
"log/slog"
"go.uber.org/zap"
)
func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error) {
@@ -34,7 +34,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
opts.Rule,
opts.Reader,
opts.Querier,
opts.Logger,
opts.SLogger,
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
baserules.WithSQLStore(opts.SQLStore),
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
@@ -57,7 +57,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
ruleId,
opts.OrgID,
opts.Rule,
opts.Logger,
opts.SLogger,
opts.Reader,
opts.ManagerOpts.Prometheus,
baserules.WithSQLStore(opts.SQLStore),
@@ -82,7 +82,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
opts.Rule,
opts.Reader,
opts.Querier,
opts.Logger,
opts.SLogger,
opts.Cache,
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
baserules.WithSQLStore(opts.SQLStore),
@@ -142,7 +142,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
parsedRule,
opts.Reader,
opts.Querier,
opts.Logger,
opts.SLogger,
baserules.WithSendAlways(),
baserules.WithSendUnmatched(),
baserules.WithSQLStore(opts.SQLStore),
@@ -151,7 +151,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
)
if err != nil {
slog.Error("failed to prepare a new threshold rule for test", "name", alertname, "error", err)
zap.L().Error("failed to prepare a new threshold rule for test", zap.String("name", alertname), zap.Error(err))
return 0, basemodel.BadRequest(err)
}
@@ -162,7 +162,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
alertname,
opts.OrgID,
parsedRule,
opts.Logger,
opts.SLogger,
opts.Reader,
opts.ManagerOpts.Prometheus,
baserules.WithSendAlways(),
@@ -173,7 +173,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
)
if err != nil {
slog.Error("failed to prepare a new promql rule for test", "name", alertname, "error", err)
zap.L().Error("failed to prepare a new promql rule for test", zap.String("name", alertname), zap.Error(err))
return 0, basemodel.BadRequest(err)
}
} else if parsedRule.RuleType == ruletypes.RuleTypeAnomaly {
@@ -184,7 +184,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
parsedRule,
opts.Reader,
opts.Querier,
opts.Logger,
opts.SLogger,
opts.Cache,
baserules.WithSendAlways(),
baserules.WithSendUnmatched(),
@@ -193,7 +193,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
baserules.WithMetadataStore(opts.ManagerOpts.MetadataStore),
)
if err != nil {
slog.Error("failed to prepare a new anomaly rule for test", "name", alertname, "error", err)
zap.L().Error("failed to prepare a new anomaly rule for test", zap.String("name", alertname), zap.Error(err))
return 0, basemodel.BadRequest(err)
}
} else {
@@ -205,7 +205,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
alertsFound, err := rule.Eval(ctx, ts)
if err != nil {
slog.Error("evaluating rule failed", "rule", rule.Name(), "error", err)
zap.L().Error("evaluating rule failed", zap.String("rule", rule.Name()), zap.Error(err))
return 0, basemodel.InternalError(fmt.Errorf("rule evaluation failed"))
}
rule.SendAlerts(ctx, ts, 0, time.Minute, opts.NotifyFunc)

View File

@@ -8,12 +8,12 @@ import (
"sync/atomic"
"time"
"log/slog"
"github.com/ClickHouse/clickhouse-go/v2"
"github.com/go-co-op/gocron"
"github.com/google/uuid"
"go.uber.org/zap"
"github.com/SigNoz/signoz/ee/query-service/model"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/organization"
@@ -76,19 +76,19 @@ func (lm *Manager) Start(ctx context.Context) error {
func (lm *Manager) UploadUsage(ctx context.Context) {
organizations, err := lm.orgGetter.ListByOwnedKeyRange(ctx)
if err != nil {
slog.ErrorContext(ctx, "failed to get organizations", "error", err)
zap.L().Error("failed to get organizations", zap.Error(err))
return
}
for _, organization := range organizations {
// check if license is present or not
license, err := lm.licenseService.GetActive(ctx, organization.ID)
if err != nil {
slog.ErrorContext(ctx, "failed to get active license", "error", err)
zap.L().Error("failed to get active license", zap.Error(err))
return
}
if license == nil {
// we will not start the usage reporting if license is not present.
slog.InfoContext(ctx, "no license present, skipping usage reporting")
zap.L().Info("no license present, skipping usage reporting")
return
}
@@ -115,7 +115,7 @@ func (lm *Manager) UploadUsage(ctx context.Context) {
dbusages := []model.UsageDB{}
err := lm.clickhouseConn.Select(ctx, &dbusages, fmt.Sprintf(query, db, db), time.Now().Add(-(24 * time.Hour)))
if err != nil && !strings.Contains(err.Error(), "doesn't exist") {
slog.ErrorContext(ctx, "failed to get usage from clickhouse", "error", err)
zap.L().Error("failed to get usage from clickhouse: %v", zap.Error(err))
return
}
for _, u := range dbusages {
@@ -125,24 +125,24 @@ func (lm *Manager) UploadUsage(ctx context.Context) {
}
if len(usages) <= 0 {
slog.InfoContext(ctx, "no snapshots to upload, skipping")
zap.L().Info("no snapshots to upload, skipping.")
return
}
slog.InfoContext(ctx, "uploading usage data")
zap.L().Info("uploading usage data")
usagesPayload := []model.Usage{}
for _, usage := range usages {
usageDataBytes, err := encryption.Decrypt([]byte(usage.ExporterID[:32]), []byte(usage.Data))
if err != nil {
slog.ErrorContext(ctx, "error while decrypting usage data", "error", err)
zap.L().Error("error while decrypting usage data: %v", zap.Error(err))
return
}
usageData := model.Usage{}
err = json.Unmarshal(usageDataBytes, &usageData)
if err != nil {
slog.ErrorContext(ctx, "error while unmarshalling usage data", "error", err)
zap.L().Error("error while unmarshalling usage data: %v", zap.Error(err))
return
}
@@ -163,13 +163,13 @@ func (lm *Manager) UploadUsage(ctx context.Context) {
body, errv2 := json.Marshal(payload)
if errv2 != nil {
slog.ErrorContext(ctx, "error while marshalling usage payload", "error", errv2)
zap.L().Error("error while marshalling usage payload: %v", zap.Error(errv2))
return
}
errv2 = lm.zeus.PutMeters(ctx, payload.LicenseKey.String(), body)
if errv2 != nil {
slog.ErrorContext(ctx, "failed to upload usage", "error", errv2)
zap.L().Error("failed to upload usage: %v", zap.Error(errv2))
// not returning error here since it is captured in the failed count
return
}
@@ -179,7 +179,7 @@ func (lm *Manager) UploadUsage(ctx context.Context) {
func (lm *Manager) Stop(ctx context.Context) {
lm.scheduler.Stop()
slog.InfoContext(ctx, "sending usage data before shutting down")
zap.L().Info("sending usage data before shutting down")
// send usage before shutting down
lm.UploadUsage(ctx)
atomic.StoreUint32(&locker, stateUnlocked)

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 214 KiB

View File

@@ -302,6 +302,7 @@ function CustomTimePicker({
): void => {
event?.preventDefault();
event?.stopPropagation();
// check if the entered time is in the format of 1m, 2h, 3d, 4w
const isTimeDurationShortHandFormat = /^(\d+)([mhdw])$/.test(inputValue);

View File

@@ -1,6 +1,5 @@
// ** Helpers
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
import { defaultTraceSelectedColumns } from 'container/OptionsMenu/constants';
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
import { IAttributeValuesResponse } from 'types/api/queryBuilder/getAttributesValues';
@@ -549,49 +548,3 @@ export const DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY: Record<
[DataTypes.ArrayBool]: 'boolAttributeValues',
[DataTypes.EMPTY]: 'stringAttributeValues',
};
export const listViewInitialLogQuery: Query = {
...initialQueriesMap.logs,
builder: {
...initialQueriesMap.logs.builder,
queryData: [
{
...initialQueriesMap.logs.builder.queryData[0],
aggregateOperator: LogsAggregatorOperator.NOOP,
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
offset: 0,
pageSize: 100,
},
],
},
};
export const PANEL_TYPES_INITIAL_QUERY: Record<PANEL_TYPES, Query> = {
[PANEL_TYPES.TIME_SERIES]: initialQueriesMap.metrics,
[PANEL_TYPES.VALUE]: initialQueriesMap.metrics,
[PANEL_TYPES.TABLE]: initialQueriesMap.metrics,
[PANEL_TYPES.LIST]: listViewInitialLogQuery,
[PANEL_TYPES.TRACE]: initialQueriesMap.traces,
[PANEL_TYPES.BAR]: initialQueriesMap.metrics,
[PANEL_TYPES.PIE]: initialQueriesMap.metrics,
[PANEL_TYPES.HISTOGRAM]: initialQueriesMap.metrics,
[PANEL_TYPES.EMPTY_WIDGET]: initialQueriesMap.metrics,
};
export const listViewInitialTraceQuery: Query = {
// it should be the above commented query
...initialQueriesMap.traces,
builder: {
...initialQueriesMap.traces.builder,
queryData: [
{
...initialQueriesMap.traces.builder.queryData[0],
aggregateOperator: LogsAggregatorOperator.NOOP,
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
offset: 0,
pageSize: 10,
selectColumns: defaultTraceSelectedColumns,
},
],
},
};

View File

@@ -1,4 +1,4 @@
.panel-type-selection-modal {
.graph-selection {
.ant-modal-content {
width: 515px;
max-height: 646px;
@@ -76,11 +76,6 @@
content: none;
}
}
.panel-type-text {
text-align: center;
margin-top: 1rem;
}
}
}
@@ -119,7 +114,7 @@
}
.lightMode {
.panel-type-selection-modal {
.graph-selection {
.ant-modal-content {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);

View File

@@ -0,0 +1,50 @@
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { defaultTraceSelectedColumns } from 'container/OptionsMenu/constants';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { LogsAggregatorOperator } from 'types/common/queryBuilder';
export const PANEL_TYPES_INITIAL_QUERY = {
[PANEL_TYPES.TIME_SERIES]: initialQueriesMap.metrics,
[PANEL_TYPES.VALUE]: initialQueriesMap.metrics,
[PANEL_TYPES.TABLE]: initialQueriesMap.metrics,
[PANEL_TYPES.LIST]: initialQueriesMap.logs,
[PANEL_TYPES.TRACE]: initialQueriesMap.traces,
[PANEL_TYPES.BAR]: initialQueriesMap.metrics,
[PANEL_TYPES.PIE]: initialQueriesMap.metrics,
[PANEL_TYPES.HISTOGRAM]: initialQueriesMap.metrics,
[PANEL_TYPES.EMPTY_WIDGET]: initialQueriesMap.metrics,
};
export const listViewInitialLogQuery: Query = {
...initialQueriesMap.logs,
builder: {
...initialQueriesMap.logs.builder,
queryData: [
{
...initialQueriesMap.logs.builder.queryData[0],
aggregateOperator: LogsAggregatorOperator.NOOP,
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
offset: 0,
pageSize: 100,
},
],
},
};
export const listViewInitialTraceQuery: Query = {
// it should be the above commented query
...initialQueriesMap.traces,
builder: {
...initialQueriesMap.traces.builder,
queryData: [
{
...initialQueriesMap.traces.builder.queryData[0],
aggregateOperator: LogsAggregatorOperator.NOOP,
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
offset: 0,
pageSize: 10,
selectColumns: defaultTraceSelectedColumns,
},
],
},
};

View File

@@ -0,0 +1,94 @@
import { Card, Modal } from 'antd';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import createQueryParams from 'lib/createQueryParams';
import history from 'lib/history';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { LogsAggregatorOperator } from 'types/common/queryBuilder';
import { v4 as uuid } from 'uuid';
import { PANEL_TYPES_INITIAL_QUERY } from './constants';
import menuItems from './menuItems';
import { Text } from './styles';
import './ComponentSlider.styles.scss';
function DashboardGraphSlider(): JSX.Element {
const { handleToggleDashboardSlider, isDashboardSliderOpen } = useDashboard();
const onClickHandler = (name: PANEL_TYPES) => (): void => {
const id = uuid();
handleToggleDashboardSlider(false);
logEvent('Dashboard Detail: New panel type selected', {
// dashboardId: '',
// dashboardName: '',
// numberOfPanels: 0, // todo - at this point we don't know these attributes
panelType: name,
widgetId: id,
});
const queryParamsLog = {
graphType: name,
widgetId: id,
[QueryParams.compositeQuery]: JSON.stringify({
...PANEL_TYPES_INITIAL_QUERY[name],
builder: {
...PANEL_TYPES_INITIAL_QUERY[name].builder,
queryData: [
{
...PANEL_TYPES_INITIAL_QUERY[name].builder.queryData[0],
aggregateOperator: LogsAggregatorOperator.NOOP,
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
offset: 0,
pageSize: 100,
},
],
},
}),
};
const queryParams = {
graphType: name,
widgetId: id,
[QueryParams.compositeQuery]: JSON.stringify(
PANEL_TYPES_INITIAL_QUERY[name],
),
};
if (name === PANEL_TYPES.LIST) {
history.push(
`${history.location.pathname}/new?${createQueryParams(queryParamsLog)}`,
);
} else {
history.push(
`${history.location.pathname}/new?${createQueryParams(queryParams)}`,
);
}
};
const handleCardClick = (panelType: PANEL_TYPES): void => {
onClickHandler(panelType)();
};
return (
<Modal
open={isDashboardSliderOpen}
onCancel={(): void => {
handleToggleDashboardSlider(false);
}}
rootClassName="graph-selection"
footer={null}
title="New Panel"
>
<div className="panel-selection">
{menuItems.map(({ name, icon, display }) => (
<Card onClick={(): void => handleCardClick(name)} id={name} key={name}>
{icon}
<Text>{display}</Text>
</Card>
))}
</div>
</Modal>
);
}
export default DashboardGraphSlider;

View File

@@ -9,7 +9,7 @@ import {
Table,
} from 'lucide-react';
export const PanelTypesWithData: ItemsProps[] = [
const Items: ItemsProps[] = [
{
name: PANEL_TYPES.TIME_SERIES,
icon: <LineChart size={16} color={Color.BG_ROBIN_400} />,
@@ -52,3 +52,5 @@ export interface ItemsProps {
icon: JSX.Element;
display: string;
}
export default Items;

View File

@@ -0,0 +1,41 @@
import { Card as CardComponent, Typography } from 'antd';
import styled from 'styled-components';
export const Container = styled.div`
display: flex;
justify-content: right;
gap: 8px;
margin-bottom: 12px;
`;
export const Card = styled(CardComponent)`
min-height: 80px;
min-width: 120px;
overflow-y: auto;
cursor: pointer;
transition: transform 0.2s;
.ant-card-body {
padding: 12px;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
.ant-typography {
font-size: 12px;
font-weight: 600;
}
}
&:hover {
transform: scale(1.05);
border: 1px solid var(--bg-robin-400);
}
`;
export const Text = styled(Typography)`
text-align: center;
margin-top: 1rem;
`;

View File

@@ -182,7 +182,9 @@ describe('Dashboard landing page actions header tests', () => {
(useLocation as jest.Mock).mockReturnValue(mockLocation);
const mockContextValue: IDashboardContext = {
isDashboardSliderOpen: false,
isDashboardLocked: false,
handleToggleDashboardSlider: jest.fn(),
handleDashboardLockToggle: jest.fn(),
dashboardResponse: {} as IDashboardContext['dashboardResponse'],
selectedDashboard: (getDashboardById.data as unknown) as Dashboard,

View File

@@ -40,7 +40,6 @@ import {
} from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
import { sortLayout } from 'providers/Dashboard/util';
import { DashboardData } from 'types/api/dashboard/getAll';
import { Props } from 'types/api/dashboard/update';
@@ -49,10 +48,10 @@ import { ComponentTypes } from 'utils/permission';
import { v4 as uuid } from 'uuid';
import DashboardHeader from '../components/DashboardHeader/DashboardHeader';
import DashboardGraphSlider from '../ComponentsSlider';
import DashboardSettings from '../DashboardSettings';
import { Base64Icons } from '../DashboardSettings/General/utils';
import DashboardVariableSelection from '../DashboardVariablesSelection';
import PanelTypeSelectionModal from '../PanelTypeSelectionModal';
import SettingsDrawer from './SettingsDrawer';
import { VariablesSettingsTab } from './types';
import {
@@ -70,9 +69,6 @@ interface DashboardDescriptionProps {
// eslint-disable-next-line sonarjs/cognitive-complexity
function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
const { handle } = props;
const setIsPanelTypeSelectionModalOpen = usePanelTypeSelectionModalStore(
(s) => s.setIsPanelTypeSelectionModalOpen,
);
const {
selectedDashboard,
panelMap,
@@ -81,6 +77,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
setLayouts,
isDashboardLocked,
setSelectedDashboard,
handleToggleDashboardSlider,
handleDashboardLockToggle,
} = useDashboard();
@@ -148,14 +145,14 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
const [addPanelPermission] = useComponentPermission(permissions, userRole);
const onEmptyWidgetHandler = useCallback(() => {
setIsPanelTypeSelectionModalOpen(true);
handleToggleDashboardSlider(true);
logEvent('Dashboard Detail: Add new panel clicked', {
dashboardId: selectedDashboard?.id,
dashboardName: selectedDashboard?.data.title,
numberOfPanels: selectedDashboard?.data.widgets?.length,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setIsPanelTypeSelectionModalOpen]);
}, [handleToggleDashboardSlider]);
const handleLockDashboardToggle = (): void => {
setIsDashbordSettingsOpen(false);
@@ -524,7 +521,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
<DashboardVariableSelection />
</section>
)}
<PanelTypeSelectionModal />
<DashboardGraphSlider />
<Modal
open={isRenameDashboardOpen}

View File

@@ -9,6 +9,7 @@ import {
} from 'hooks/dashboard/useDashboardVariables';
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
import { updateDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
import {
enqueueDescendantsOfVariable,
@@ -29,7 +30,7 @@ function DashboardVariableSelection(): JSX.Element | null {
updateLocalStorageDashboardVariables,
} = useDashboard();
const { updateUrlVariable } = useVariablesFromUrl();
const { updateUrlVariable, getUrlVariables } = useVariablesFromUrl();
const { dashboardVariables } = useDashboardVariables();
const dashboardId = useDashboardVariablesSelector(
@@ -49,6 +50,15 @@ function DashboardVariableSelection(): JSX.Element | null {
(state) => state.globalTime,
);
useEffect(() => {
// Initialize variables with default values if not in URL
initializeDefaultVariables(
dashboardVariables,
getUrlVariables,
updateUrlVariable,
);
}, [getUrlVariables, updateUrlVariable, dashboardVariables]);
// Memoize the order key to avoid unnecessary triggers
const variableOrderKey = useMemo(() => {
const queryVariableOrderKey = dependencyData?.order?.join(',') ?? '';

View File

@@ -1,68 +0,0 @@
import { memo } from 'react';
import { Card, Modal, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES, PANEL_TYPES_INITIAL_QUERY } from 'constants/queryBuilder';
import createQueryParams from 'lib/createQueryParams';
import history from 'lib/history';
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
import { v4 as uuid } from 'uuid';
import { PanelTypesWithData } from './menuItems';
import './PanelTypeSelectionModal.styles.scss';
function PanelTypeSelectionModal(): JSX.Element {
const {
isPanelTypeSelectionModalOpen,
setIsPanelTypeSelectionModalOpen,
} = usePanelTypeSelectionModalStore();
const onClickHandler = (name: PANEL_TYPES) => (): void => {
const id = uuid();
setIsPanelTypeSelectionModalOpen(false);
logEvent('Dashboard Detail: New panel type selected', {
panelType: name,
widgetId: id,
});
const queryParams = {
graphType: name,
widgetId: id,
[QueryParams.compositeQuery]: JSON.stringify(
PANEL_TYPES_INITIAL_QUERY[name],
),
};
history.push(
`${history.location.pathname}/new?${createQueryParams(queryParams)}`,
);
};
const handleCardClick = (panelType: PANEL_TYPES): void => {
onClickHandler(panelType)();
};
return (
<Modal
open={isPanelTypeSelectionModalOpen}
onCancel={(): void => {
setIsPanelTypeSelectionModalOpen(false);
}}
rootClassName="panel-type-selection-modal"
footer={null}
title="New Panel"
>
<div className="panel-selection">
{PanelTypesWithData.map(({ name, icon, display }) => (
<Card onClick={(): void => handleCardClick(name)} id={name} key={name}>
{icon}
<Typography className="panel-type-text">{display}</Typography>
</Card>
))}
</div>
</Modal>
);
}
export default memo(PanelTypeSelectionModal);

View File

@@ -9,18 +9,17 @@ import DashboardSettings from 'container/DashboardContainer/DashboardSettings';
import useComponentPermission from 'hooks/useComponentPermission';
import { useAppContext } from 'providers/App/App';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
import { ROLES, USER_ROLES } from 'types/roles';
import { ComponentTypes } from 'utils/permission';
import './DashboardEmptyState.styles.scss';
export default function DashboardEmptyState(): JSX.Element {
const setIsPanelTypeSelectionModalOpen = usePanelTypeSelectionModalStore(
(s) => s.setIsPanelTypeSelectionModalOpen,
);
const { selectedDashboard, isDashboardLocked } = useDashboard();
const {
selectedDashboard,
isDashboardLocked,
handleToggleDashboardSlider,
} = useDashboard();
const variablesSettingsTabHandle = useRef<VariablesSettingsTab>(null);
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] = useState<boolean>(
@@ -42,14 +41,14 @@ export default function DashboardEmptyState(): JSX.Element {
const [addPanelPermission] = useComponentPermission(permissions, userRole);
const onEmptyWidgetHandler = useCallback(() => {
setIsPanelTypeSelectionModalOpen(true);
handleToggleDashboardSlider(true);
logEvent('Dashboard Detail: Add new panel clicked', {
dashboardId: selectedDashboard?.id,
dashboardName: selectedDashboard?.data.title,
numberOfPanels: selectedDashboard?.data.widgets?.length,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setIsPanelTypeSelectionModalOpen]);
}, [handleToggleDashboardSlider]);
const onConfigureClick = useCallback((): void => {
setIsSettingsDrawerOpen(true);

View File

@@ -2,7 +2,7 @@ import { useCallback } from 'react';
import { Select, Typography } from 'antd';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { PanelTypesWithData } from 'container/DashboardContainer/PanelTypeSelectionModal/menuItems';
import GraphTypes from 'container/DashboardContainer/ComponentsSlider/menuItems';
import { handleQueryChange } from 'container/NewWidget/utils';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@@ -59,7 +59,7 @@ function PanelTypeSelector({
data-testid="panel-change-select"
disabled={disabled}
>
{PanelTypesWithData.map((item) => (
{GraphTypes.map((item) => (
<Option key={item.name} value={item.name}>
<div className="view-panel-select-option">
<div className="icon">{item.icon}</div>

View File

@@ -5,7 +5,6 @@ import useComponentPermission from 'hooks/useComponentPermission';
import { EllipsisIcon, PenLine, Plus, X } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
import { setSelectedRowWidgetId } from 'providers/Dashboard/helpers/selectedRowWidgetIdHelper';
import { ROLES, USER_ROLES } from 'types/roles';
import { ComponentTypes } from 'utils/permission';
@@ -35,11 +34,11 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
} = props;
const [isRowSettingsOpen, setIsRowSettingsOpen] = useState<boolean>(false);
const setIsPanelTypeSelectionModalOpen = usePanelTypeSelectionModalStore(
(s) => s.setIsPanelTypeSelectionModalOpen,
);
const { selectedDashboard, isDashboardLocked } = useDashboard();
const {
handleToggleDashboardSlider,
selectedDashboard,
isDashboardLocked,
} = useDashboard();
const permissions: ComponentTypes[] = ['add_panel'];
const { user } = useAppContext();
@@ -88,7 +87,7 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
}
setSelectedRowWidgetId(selectedDashboard.id, id);
setIsPanelTypeSelectionModalOpen(true);
handleToggleDashboardSlider(true);
}}
>
New Panel

View File

@@ -15,7 +15,6 @@ import ROUTES from 'constants/routes';
import { getMetricsListQuery } from 'container/MetricsExplorer/Summary/utils';
import { useGetMetricsList } from 'hooks/metricsExplorer/useGetMetricsList';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useIsDarkMode } from 'hooks/useDarkMode';
import history from 'lib/history';
import cloneDeep from 'lodash-es/cloneDeep';
import { AnimatePresence } from 'motion/react';
@@ -44,7 +43,6 @@ const homeInterval = 30 * 60 * 1000;
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function Home(): JSX.Element {
const { user } = useAppContext();
const isDarkMode = useIsDarkMode();
const [startTime, setStartTime] = useState<number | null>(null);
const [endTime, setEndTime] = useState<number | null>(null);
@@ -682,11 +680,7 @@ export default function Home(): JSX.Element {
<div className="checklist-img-container">
<img
src={
isDarkMode
? '/Images/allInOne.svg'
: '/Images/allInOneLightMode.svg'
}
src="/Images/allInOne.svg"
alt="checklist-img"
className="checklist-img"
/>

View File

@@ -1,7 +1,5 @@
.column-unit-selector {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 16px;
.heading {
color: var(--bg-vanilla-400);
@@ -32,11 +30,6 @@
width: 100%;
}
}
&-content {
display: flex;
flex-direction: column;
gap: 12px;
}
}
.lightMode {

View File

@@ -72,24 +72,22 @@ export function ColumnUnitSelector(
return (
<section className="column-unit-selector">
<Typography.Text className="heading">Column Units</Typography.Text>
<div className="column-unit-selector-content">
{aggregationQueries.map(({ value, label }) => {
const baseQueryName = value.split('.')[0];
return (
<YAxisUnitSelectorV2
value={columnUnits[value] || ''}
onSelect={(unitValue: string): void =>
handleColumnUnitSelect(value, unitValue)
}
fieldLabel={label}
key={value}
selectedQueryName={baseQueryName}
// Update the column unit value automatically only in create mode
shouldUpdateYAxisUnit={isNewDashboard}
/>
);
})}
</div>
{aggregationQueries.map(({ value, label }) => {
const baseQueryName = value.split('.')[0];
return (
<YAxisUnitSelectorV2
value={columnUnits[value] || ''}
onSelect={(unitValue: string): void =>
handleColumnUnitSelect(value, unitValue)
}
fieldLabel={label}
key={value}
selectedQueryName={baseQueryName}
// Update the column unit value automatically only in create mode
shouldUpdateYAxisUnit={isNewDashboard}
/>
);
})}
</section>
);
}

View File

@@ -56,6 +56,9 @@ describe('ContextLinks Component', () => {
/>,
);
// Check that the component renders
expect(screen.getByText('Context Links')).toBeInTheDocument();
// Check that the add button is present
expect(
screen.getByRole('button', { name: /context link/i }),

View File

@@ -14,7 +14,7 @@ import {
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Button, Modal } from 'antd';
import { Button, Modal, Typography } from 'antd';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { GripVertical, Pencil, Plus, Trash2 } from 'lucide-react';
import {
@@ -134,16 +134,11 @@ function ContextLinks({
return (
<div className="context-links-container">
<Typography.Text className="context-links-text">
Context Links
</Typography.Text>
<div className="context-links-list">
<Button
type="default"
className="add-context-link-button"
icon={<Plus size={12} />}
style={{ width: '100%' }}
onClick={handleAddContextLink}
>
Add Context Link
</Button>
<OverlayScrollbar>
<DndContext
sensors={sensors}
@@ -165,6 +160,16 @@ function ContextLinks({
</SortableContext>
</DndContext>
</OverlayScrollbar>
{/* button to add context link */}
<Button
type="primary"
className="add-context-link-button"
icon={<Plus size={12} />}
onClick={handleAddContextLink}
>
Context Link
</Button>
</div>
<Modal

View File

@@ -2,6 +2,7 @@
display: flex;
flex-direction: column;
gap: 16px;
margin: 12px;
}
.context-links-text {
@@ -109,7 +110,10 @@
}
.add-context-link-button {
width: 100%;
display: flex;
align-items: center;
margin: auto;
width: fit-content;
}
.lightMode {

View File

@@ -1,7 +1,6 @@
.right-container {
display: flex;
flex-direction: column;
padding-bottom: 48px;
.header {
display: flex;
@@ -25,14 +24,14 @@
letter-spacing: -0.07px;
}
}
.control-container {
display: flex;
flex-direction: column;
gap: 8px;
}
.name-description {
padding: 0 0 4px 0;
display: flex;
flex-direction: column;
padding: 12px 12px 16px 12px;
border-top: 1px solid var(--bg-slate-500);
border-bottom: 1px solid var(--bg-slate-500);
gap: 8px;
.typography {
color: var(--bg-vanilla-400);
@@ -89,6 +88,9 @@
.panel-config {
display: flex;
flex-direction: column;
padding: 12px 12px 16px 12px;
gap: 8px;
border-bottom: 1px solid var(--bg-slate-500);
.typography {
color: var(--bg-vanilla-400);
@@ -102,7 +104,6 @@
}
.panel-type-select {
width: 100%;
.ant-select-selector {
display: flex;
height: 32px;
@@ -136,6 +137,7 @@
}
.fill-gaps {
margin-top: 16px;
display: flex;
padding: 12px;
justify-content: space-between;
@@ -154,24 +156,31 @@
letter-spacing: 0.52px;
text-transform: uppercase;
}
.fill-gaps-text-description {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
opacity: 0.6;
line-height: 16px; /* 133.333% */
}
}
.log-scale,
.decimal-precision-selector,
.legend-position {
.decimal-precision-selector {
margin-top: 16px;
display: flex;
justify-content: space-between;
flex-direction: column;
gap: 8px;
}
.legend-position {
margin-top: 16px;
display: flex;
justify-content: space-between;
flex-direction: column;
gap: 8px;
}
.legend-colors {
margin-top: 16px;
}
.panel-time-text {
margin-top: 16px;
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
font-size: 13px;
@@ -184,6 +193,7 @@
.y-axis-unit-selector,
.y-axis-unit-selector-v2 {
margin-top: 16px;
display: flex;
flex-direction: column;
gap: 8px;
@@ -268,8 +278,11 @@
}
.stack-chart {
flex-direction: row;
margin-top: 16px;
display: flex;
justify-content: space-between;
gap: 8px;
.label {
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
@@ -283,6 +296,11 @@
}
.bucket-config {
margin-top: 16px;
display: flex;
flex-direction: column;
gap: 8px;
.label {
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
@@ -334,13 +352,16 @@
}
}
.context-links {
border-bottom: 1px solid var(--bg-slate-500);
}
.alerts {
display: flex;
padding: 12px;
align-items: center;
justify-content: space-between;
padding: 12px;
min-height: 44px;
border-top: 1px solid var(--bg-slate-500);
border-bottom: 1px solid var(--bg-slate-500);
cursor: pointer;
.left-section {
@@ -366,16 +387,6 @@
color: var(--bg-vanilla-400);
}
}
.context-links {
padding: 12px 12px 16px 12px;
border-bottom: 1px solid var(--bg-slate-500);
}
.thresholds-section {
padding: 12px 12px 16px 12px;
border-top: 1px solid var(--bg-slate-500);
}
}
.select-option {
@@ -407,6 +418,9 @@
}
.name-description {
border-top: 1px solid var(--bg-vanilla-300);
border-bottom: 1px solid var(--bg-vanilla-300);
.typography {
color: var(--bg-ink-400);
}
@@ -427,6 +441,8 @@
}
.panel-config {
border-bottom: 1px solid var(--bg-vanilla-300);
.typography {
color: var(--bg-ink-400);
}
@@ -462,9 +478,6 @@
.fill-gaps-text {
color: var(--bg-ink-400);
}
.fill-gaps-text-description {
color: var(--bg-ink-400);
}
}
.bucket-config {
@@ -517,7 +530,7 @@
}
.alerts {
border-top: 1px solid var(--bg-vanilla-300);
border-bottom: 1px solid var(--bg-vanilla-300);
.left-section {
.bell-icon {
@@ -536,10 +549,6 @@
.context-links {
border-bottom: 1px solid var(--bg-vanilla-300);
}
.thresholds-section {
border-top: 1px solid var(--bg-vanilla-300);
}
}
.select-option {

View File

@@ -1,4 +1,5 @@
.threshold-selector-container {
padding: 12px;
padding-bottom: 80px;
.threshold-select {

View File

@@ -1,10 +1,10 @@
import { useCallback } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { Button } from 'antd';
import { Typography } from 'antd';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useGetQueryLabels } from 'hooks/useGetQueryLabels';
import { Plus } from 'lucide-react';
import { Antenna, Plus } from 'lucide-react';
import { v4 as uuid } from 'uuid';
import Threshold from './Threshold';
@@ -68,14 +68,11 @@ function ThresholdSelector({
<DndProvider backend={HTML5Backend}>
<div className="threshold-selector-container">
<div className="threshold-select" onClick={addThresholdHandler}>
<Button
type="default"
icon={<Plus size={14} />}
style={{ width: '100%' }}
onClick={addThresholdHandler}
>
Add Threshold
</Button>
<div className="left-section">
<Antenna size={14} className="icon" />
<Typography.Text className="text">Thresholds</Typography.Text>
</div>
<Plus size={14} onClick={addThresholdHandler} className="icon" />
</div>
{thresholds.map((threshold, idx) => (
<Threshold

View File

@@ -1,68 +0,0 @@
.settings-section {
border-top: 1px solid var(--bg-slate-500);
}
.settings-section-header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 12px 12px;
min-height: 44px;
background: transparent;
border: none;
outline: none;
cursor: pointer;
.settings-section-title {
display: flex;
align-items: center;
gap: 8px;
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
font-size: 13px;
font-weight: 400;
text-transform: uppercase;
}
.chevron-icon {
color: var(--bg-vanilla-400);
transition: transform 0.2s ease-in-out;
&.open {
transform: rotate(180deg);
}
}
}
.settings-section-content {
padding: 0 12px 0 12px;
display: flex;
flex-direction: column;
gap: 20px;
max-height: 0;
overflow: hidden;
opacity: 0;
transition: max-height 0.25s ease, opacity 0.25s ease, padding 0.25s ease;
&.open {
padding-bottom: 24px;
max-height: 1000px;
opacity: 1;
}
}
.lightMode {
.settings-section-header {
.chevron-icon {
color: var(--bg-ink-400);
}
.settings-section-title {
color: var(--bg-ink-400);
}
}
.settings-section {
border-top: 1px solid var(--bg-vanilla-300);
}
}

View File

@@ -1,51 +0,0 @@
import { ReactNode, useState } from 'react';
import { ChevronDown } from 'lucide-react';
import './SettingsSection.styles.scss';
export interface SettingsSectionProps {
title: string;
defaultOpen?: boolean;
children: ReactNode;
icon?: ReactNode;
}
function SettingsSection({
title,
defaultOpen = false,
children,
icon,
}: SettingsSectionProps): JSX.Element {
const [isOpen, setIsOpen] = useState(defaultOpen);
const toggleOpen = (): void => {
setIsOpen((prev) => !prev);
};
return (
<section className="settings-section">
<button
type="button"
className="settings-section-header"
onClick={toggleOpen}
>
<span className="settings-section-title">
{icon ? icon : null} {title}
</span>
<ChevronDown
size={16}
className={isOpen ? 'chevron-icon open' : 'chevron-icon'}
/>
</button>
<div
className={
isOpen ? 'settings-section-content open' : 'settings-section-content'
}
>
{children}
</div>
</section>
);
}
export default SettingsSection;

View File

@@ -14,30 +14,23 @@ import {
Input,
InputNumber,
Select,
Space,
Switch,
Typography,
} from 'antd';
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
import TimePreference from 'components/TimePreferenceDropDown';
import { PANEL_TYPES, PanelDisplay } from 'constants/queryBuilder';
import {
import GraphTypes, {
ItemsProps,
PanelTypesWithData,
} from 'container/DashboardContainer/PanelTypeSelectionModal/menuItems';
} from 'container/DashboardContainer/ComponentsSlider/menuItems';
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import {
Antenna,
Axis3D,
ConciergeBell,
Layers,
LayoutDashboard,
LineChart,
Link,
Pencil,
Plus,
SlidersHorizontal,
Spline,
SquareArrowOutUpRight,
} from 'lucide-react';
@@ -53,7 +46,6 @@ import { DataSource } from 'types/common/queryBuilder';
import { popupContainer } from 'utils/selectPopupContainer';
import { ColumnUnitSelector } from './ColumnUnitSelector/ColumnUnitSelector';
import SettingsSection from './components/SettingsSection/SettingsSection';
import {
panelTypeVsBucketConfig,
panelTypeVsColumnUnitPreferences,
@@ -151,7 +143,7 @@ function RightContainer({
);
const selectedGraphType =
PanelTypesWithData.find((e) => e.name === selectedGraph)?.display || '';
GraphTypes.find((e) => e.name === selectedGraph)?.display || '';
const onCreateAlertsHandler = useCreateAlerts(selectedWidget, 'panelView');
@@ -177,7 +169,7 @@ function RightContainer({
const { currentQuery } = useQueryBuilder();
const [graphTypes, setGraphTypes] = useState<ItemsProps[]>(PanelTypesWithData);
const [graphTypes, setGraphTypes] = useState<ItemsProps[]>(GraphTypes);
const dashboardVariableOptions = useMemo<VariableOption[]>(() => {
return Object.entries(dashboardVariables).map(([, value]) => ({
@@ -186,21 +178,6 @@ function RightContainer({
}));
}, [dashboardVariables]);
const isAxisSectionVisible = useMemo(() => allowSoftMinMax || allowLogScale, [
allowSoftMinMax,
allowLogScale,
]);
const isFormattingSectionVisible = useMemo(
() => allowYAxisUnit || allowDecimalPrecision || allowPanelColumnPreference,
[allowYAxisUnit, allowDecimalPrecision, allowPanelColumnPreference],
);
const isLegendSectionVisible = useMemo(
() => allowLegendPosition || allowLegendColors,
[allowLegendPosition, allowLegendColors],
);
const updateCursorAndDropdown = (value: string, pos: number): void => {
setCursorPos(pos);
const lastDollar = value.lastIndexOf('$', pos - 1);
@@ -216,15 +193,6 @@ function RightContainer({
}, 0);
};
const decimapPrecisionOptions = useMemo(() => {
return [
{ label: '0 decimals', value: PrecisionOptionsEnum.ZERO },
{ label: '1 decimal', value: PrecisionOptionsEnum.ONE },
{ label: '2 decimals', value: PrecisionOptionsEnum.TWO },
{ label: '3 decimals', value: PrecisionOptionsEnum.THREE },
];
}, []);
const handleInputCursor = (): void => {
const pos = inputRef.current?.input?.selectionStart ?? 0;
updateCursorAndDropdown(inputValue, pos);
@@ -273,7 +241,7 @@ function RightContainer({
prev.filter((graph) => graph.name !== PANEL_TYPES.LIST),
);
} else {
setGraphTypes(PanelTypesWithData);
setGraphTypes(GraphTypes);
}
}, [currentQuery]);
@@ -295,297 +263,269 @@ function RightContainer({
<div className="right-container">
<section className="header">
<div className="purple-dot" />
<Typography.Text className="header-text">Panel Settings</Typography.Text>
<Typography.Text className="header-text">Panel details</Typography.Text>
</section>
<SettingsSection title="General" defaultOpen icon={<Pencil size={14} />}>
<section className="name-description control-container">
<Typography.Text className="typography">Name</Typography.Text>
<AutoComplete
options={dashboardVariableOptions}
value={inputValue}
onChange={onInputChange}
onSelect={onSelect}
filterOption={filterOption}
style={{ width: '100%' }}
getPopupContainer={popupContainer}
placeholder="Enter the panel name here..."
open={autoCompleteOpen}
>
<Input
rootClassName="name-input"
ref={inputRef}
onSelect={handleInputCursor}
onClick={handleInputCursor}
onBlur={(): void => setAutoCompleteOpen(false)}
/>
</AutoComplete>
<Typography.Text className="typography">Description</Typography.Text>
<TextArea
placeholder="Enter the panel description here..."
bordered
allowClear
value={description}
onChange={(event): void =>
onChangeHandler(setDescription, event.target.value)
}
rootClassName="description-input"
/>
</section>
</SettingsSection>
<section className="panel-config">
<SettingsSection
title="Visualization"
defaultOpen
icon={<LayoutDashboard size={14} />}
<section className="name-description">
<Typography.Text className="typography">Name</Typography.Text>
<AutoComplete
options={dashboardVariableOptions}
value={inputValue}
onChange={onInputChange}
onSelect={onSelect}
filterOption={filterOption}
style={{ width: '100%' }}
getPopupContainer={popupContainer}
placeholder="Enter the panel name here..."
open={autoCompleteOpen}
>
<section className="panel-type control-container">
<Typography.Text className="typography">Panel Type</Typography.Text>
<Select
onChange={setGraphHandler}
value={selectedGraph}
className="panel-type-select"
data-testid="panel-change-select"
data-stacking-state={stackedBarChart ? 'true' : 'false'}
>
{graphTypes.map((item) => (
<Option key={item.name} value={item.name}>
<div className="select-option">
<div className="icon">{item.icon}</div>
<Typography.Text className="display">{item.display}</Typography.Text>
</div>
</Option>
))}
</Select>
</section>
{allowPanelTimePreference && (
<section className="panel-time-preference control-container">
<Typography.Text className="panel-time-text">
Panel Time Preference
</Typography.Text>
<TimePreference
{...{
selectedTime,
setSelectedTime,
}}
/>
</section>
)}
{allowStackingBarChart && (
<section className="stack-chart control-container">
<Typography.Text className="label">Stack series</Typography.Text>
<Switch
checked={stackedBarChart}
size="small"
onChange={(checked): void => setStackedBarChart(checked)}
/>
</section>
)}
{allowFillSpans && (
<section className="fill-gaps">
<div className="fill-gaps-text-container">
<Typography className="fill-gaps-text">Fill gaps</Typography>
<Typography.Text className="fill-gaps-text-description">
Fill gaps in data with 0 for continuity
</Typography.Text>
<Input
rootClassName="name-input"
ref={inputRef}
onSelect={handleInputCursor}
onClick={handleInputCursor}
onBlur={(): void => setAutoCompleteOpen(false)}
/>
</AutoComplete>
<Typography.Text className="typography">Description</Typography.Text>
<TextArea
placeholder="Enter the panel description here..."
bordered
allowClear
value={description}
onChange={(event): void =>
onChangeHandler(setDescription, event.target.value)
}
rootClassName="description-input"
/>
</section>
<section className="panel-config">
<Typography.Text className="typography">Panel Type</Typography.Text>
<Select
onChange={setGraphHandler}
value={selectedGraph}
style={{ width: '100%' }}
className="panel-type-select"
data-testid="panel-change-select"
data-stacking-state={stackedBarChart ? 'true' : 'false'}
>
{graphTypes.map((item) => (
<Option key={item.name} value={item.name}>
<div className="select-option">
<div className="icon">{item.icon}</div>
<Typography.Text className="display">{item.display}</Typography.Text>
</div>
<Switch
checked={isFillSpans}
size="small"
onChange={(checked): void => setIsFillSpans(checked)}
</Option>
))}
</Select>
{allowFillSpans && (
<Space className="fill-gaps">
<Typography className="fill-gaps-text">Fill gaps</Typography>
<Switch
checked={isFillSpans}
size="small"
onChange={(checked): void => setIsFillSpans(checked)}
/>
</Space>
)}
{allowPanelTimePreference && (
<>
<Typography.Text className="panel-time-text">
Panel Time Preference
</Typography.Text>
<TimePreference
{...{
selectedTime,
setSelectedTime,
}}
/>
</>
)}
{allowPanelColumnPreference && (
<ColumnUnitSelector
columnUnits={columnUnits}
setColumnUnits={setColumnUnits}
isNewDashboard={isNewDashboard}
/>
)}
{allowYAxisUnit && (
<DashboardYAxisUnitSelectorWrapper
onSelect={setYAxisUnit}
value={yAxisUnit || ''}
fieldLabel={
selectedGraphType === PanelDisplay.VALUE ||
selectedGraphType === PanelDisplay.PIE
? 'Unit'
: 'Y Axis Unit'
}
// Only update the y-axis unit value automatically in create mode
shouldUpdateYAxisUnit={isNewDashboard}
/>
)}
{allowDecimalPrecision && (
<section className="decimal-precision-selector">
<Typography.Text className="typography">
Decimal Precision
</Typography.Text>
<Select
options={[
{ label: '0 decimals', value: PrecisionOptionsEnum.ZERO },
{ label: '1 decimal', value: PrecisionOptionsEnum.ONE },
{ label: '2 decimals', value: PrecisionOptionsEnum.TWO },
{ label: '3 decimals', value: PrecisionOptionsEnum.THREE },
{ label: '4 decimals', value: PrecisionOptionsEnum.FOUR },
{ label: 'Full Precision', value: PrecisionOptionsEnum.FULL },
]}
value={decimalPrecision}
style={{ width: '100%' }}
className="panel-type-select"
defaultValue={PrecisionOptionsEnum.TWO}
onChange={(val: PrecisionOption): void => setDecimalPrecision(val)}
/>
</section>
)}
{allowSoftMinMax && (
<section className="soft-min-max">
<section className="container">
<Typography.Text className="text">Soft Min</Typography.Text>
<InputNumber
type="number"
value={softMin}
onChange={softMinHandler}
rootClassName="input"
/>
</section>
)}
</SettingsSection>
{isFormattingSectionVisible && (
<SettingsSection
title="Formatting & Units"
icon={<SlidersHorizontal size={14} />}
>
{allowYAxisUnit && (
<DashboardYAxisUnitSelectorWrapper
onSelect={setYAxisUnit}
value={yAxisUnit || ''}
fieldLabel={
selectedGraphType === PanelDisplay.VALUE ||
selectedGraphType === PanelDisplay.PIE
? 'Unit'
: 'Y Axis Unit'
}
// Only update the y-axis unit value automatically in create mode
shouldUpdateYAxisUnit={isNewDashboard}
<section className="container">
<Typography.Text className="text">Soft Max</Typography.Text>
<InputNumber
value={softMax}
type="number"
rootClassName="input"
onChange={softMaxHandler}
/>
)}
{allowDecimalPrecision && (
<section className="decimal-precision-selector control-container">
<Typography.Text className="typography">
Decimal Precision
</Typography.Text>
<Select
options={decimapPrecisionOptions}
value={decimalPrecision}
style={{ width: '100%' }}
className="panel-type-select"
defaultValue={PrecisionOptionsEnum.TWO}
onChange={(val: PrecisionOption): void => setDecimalPrecision(val)}
/>
</section>
)}
{allowPanelColumnPreference && (
<ColumnUnitSelector
columnUnits={columnUnits}
setColumnUnits={setColumnUnits}
isNewDashboard={isNewDashboard}
/>
)}
</SettingsSection>
</section>
</section>
)}
{isAxisSectionVisible && (
<SettingsSection title="Axes" icon={<Axis3D size={14} />}>
{allowSoftMinMax && (
<section className="soft-min-max">
<section className="container">
<Typography.Text className="text">Soft Min</Typography.Text>
<InputNumber
type="number"
value={softMin}
onChange={softMinHandler}
rootClassName="input"
/>
</section>
<section className="container">
<Typography.Text className="text">Soft Max</Typography.Text>
<InputNumber
value={softMax}
type="number"
rootClassName="input"
onChange={softMaxHandler}
/>
</section>
</section>
)}
{allowLogScale && (
<section className="log-scale control-container">
<Typography.Text className="typography">Y Axis Scale</Typography.Text>
<Select
onChange={(value): void =>
setIsLogScale(value === LogScale.LOGARITHMIC)
}
value={isLogScale ? LogScale.LOGARITHMIC : LogScale.LINEAR}
style={{ width: '100%' }}
className="panel-type-select"
defaultValue={LogScale.LINEAR}
>
<Option value={LogScale.LINEAR}>
<div className="select-option">
<div className="icon">
<LineChart size={16} />
</div>
<Typography.Text className="display">Linear</Typography.Text>
</div>
</Option>
<Option value={LogScale.LOGARITHMIC}>
<div className="select-option">
<div className="icon">
<Spline size={16} />
</div>
<Typography.Text className="display">Logarithmic</Typography.Text>
</div>
</Option>
</Select>
</section>
)}
</SettingsSection>
)}
{isLegendSectionVisible && (
<SettingsSection title="Legend" icon={<Layers size={14} />}>
{allowLegendPosition && (
<section className="legend-position control-container">
<Typography.Text className="typography">Position</Typography.Text>
<Select
onChange={(value: LegendPosition): void => setLegendPosition(value)}
value={legendPosition}
style={{ width: '100%' }}
className="panel-type-select"
defaultValue={LegendPosition.BOTTOM}
>
<Option value={LegendPosition.BOTTOM}>
<div className="select-option">
<Typography.Text className="display">Bottom</Typography.Text>
</div>
</Option>
<Option value={LegendPosition.RIGHT}>
<div className="select-option">
<Typography.Text className="display">Right</Typography.Text>
</div>
</Option>
</Select>
</section>
)}
{allowLegendColors && (
<section className="legend-colors">
<LegendColors
customLegendColors={customLegendColors}
setCustomLegendColors={setCustomLegendColors}
queryResponse={queryResponse}
/>
</section>
)}
</SettingsSection>
{allowStackingBarChart && (
<section className="stack-chart">
<Typography.Text className="label">Stack series</Typography.Text>
<Switch
checked={stackedBarChart}
size="small"
onChange={(checked): void => setStackedBarChart(checked)}
/>
</section>
)}
{allowBucketConfig && (
<SettingsSection title="Histogram / Buckets">
<section className="bucket-config control-container">
<Typography.Text className="label">Number of buckets</Typography.Text>
<InputNumber
value={bucketCount || null}
type="number"
min={0}
rootClassName="bucket-input"
placeholder="Default: 30"
onChange={(val): void => {
setBucketCount(val || 0);
}}
/>
<Typography.Text className="label bucket-size-label">
Bucket width
<section className="bucket-config">
<Typography.Text className="label">Number of buckets</Typography.Text>
<InputNumber
value={bucketCount || null}
type="number"
min={0}
rootClassName="bucket-input"
placeholder="Default: 30"
onChange={(val): void => {
setBucketCount(val || 0);
}}
/>
<Typography.Text className="label bucket-size-label">
Bucket width
</Typography.Text>
<InputNumber
value={bucketWidth || null}
type="number"
precision={2}
placeholder="Default: Auto"
step={0.1}
min={0.0}
rootClassName="bucket-input"
onChange={(val): void => {
setBucketWidth(val || 0);
}}
/>
<section className="combine-hist">
<Typography.Text className="label">
Merge all series into one
</Typography.Text>
<InputNumber
value={bucketWidth || null}
type="number"
precision={2}
placeholder="Default: Auto"
step={0.1}
min={0.0}
rootClassName="bucket-input"
onChange={(val): void => {
setBucketWidth(val || 0);
}}
<Switch
checked={combineHistogram}
size="small"
onChange={(checked): void => setCombineHistogram(checked)}
/>
<section className="combine-hist">
<Typography.Text className="label">
Merge all series into one
</Typography.Text>
<Switch
checked={combineHistogram}
size="small"
onChange={(checked): void => setCombineHistogram(checked)}
/>
</section>
</section>
</SettingsSection>
</section>
)}
{allowLogScale && (
<section className="log-scale">
<Typography.Text className="typography">Y Axis Scale</Typography.Text>
<Select
onChange={(value): void => setIsLogScale(value === LogScale.LOGARITHMIC)}
value={isLogScale ? LogScale.LOGARITHMIC : LogScale.LINEAR}
style={{ width: '100%' }}
className="panel-type-select"
defaultValue={LogScale.LINEAR}
>
<Option value={LogScale.LINEAR}>
<div className="select-option">
<div className="icon">
<LineChart size={16} />
</div>
<Typography.Text className="display">Linear</Typography.Text>
</div>
</Option>
<Option value={LogScale.LOGARITHMIC}>
<div className="select-option">
<div className="icon">
<Spline size={16} />
</div>
<Typography.Text className="display">Logarithmic</Typography.Text>
</div>
</Option>
</Select>
</section>
)}
{allowLegendPosition && (
<section className="legend-position">
<Typography.Text className="typography">Legend Position</Typography.Text>
<Select
onChange={(value: LegendPosition): void => setLegendPosition(value)}
value={legendPosition}
style={{ width: '100%' }}
className="panel-type-select"
defaultValue={LegendPosition.BOTTOM}
>
<Option value={LegendPosition.BOTTOM}>
<div className="select-option">
<Typography.Text className="display">Bottom</Typography.Text>
</div>
</Option>
<Option value={LegendPosition.RIGHT}>
<div className="select-option">
<Typography.Text className="display">Right</Typography.Text>
</div>
</Option>
</Select>
</section>
)}
{allowLegendColors && (
<section className="legend-colors">
<LegendColors
customLegendColors={customLegendColors}
setCustomLegendColors={setCustomLegendColors}
queryResponse={queryResponse}
/>
</section>
)}
</section>
@@ -601,25 +541,17 @@ function RightContainer({
)}
{allowContextLinks && (
<SettingsSection
title="Context Links"
icon={<Link size={14} />}
defaultOpen={!!contextLinks.linksData.length}
>
<section className="context-links">
<ContextLinks
contextLinks={contextLinks}
setContextLinks={setContextLinks}
selectedWidget={selectedWidget}
/>
</SettingsSection>
</section>
)}
{allowThreshold && (
<SettingsSection
title="Thresholds"
icon={<Antenna size={14} />}
defaultOpen={!!thresholds.length}
>
<section>
<ThresholdSelector
thresholds={thresholds}
setThresholds={setThresholds}
@@ -627,7 +559,7 @@ function RightContainer({
selectedGraph={selectedGraph}
columnUnits={columnUnits}
/>
</SettingsSection>
</section>
)}
</div>
);

View File

@@ -36,7 +36,7 @@ const checkStackSeriesState = (
expect(getByTextUtil(container, 'Stack series')).toBeInTheDocument();
const stackSeriesSection = container.querySelector(
'.stack-chart',
'section > .stack-chart',
) as HTMLElement;
expect(stackSeriesSection).toBeInTheDocument();
@@ -326,7 +326,7 @@ describe('Stacking bar in new panel', () => {
expect(getByText('Stack series')).toBeInTheDocument();
// Verify section exists
const section = container.querySelector('.stack-chart');
const section = container.querySelector('section > .stack-chart');
expect(section).toBeInTheDocument();
// Verify switch is present and enabled (ant-switch-checked)

View File

@@ -439,19 +439,6 @@ function NewWidget({
globalSelectedInterval,
]);
const navigateToDashboardPage = useCallback(() => {
const params = new URLSearchParams();
const urlVariablesQueryString = query.get(QueryParams.variables);
if (urlVariablesQueryString) {
params.set(QueryParams.variables, urlVariablesQueryString);
}
const search = params.toString() ? `?${params.toString()}` : '';
safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId }) + search);
}, [dashboardId, query, safeNavigate]);
const onClickSaveHandler = useCallback(() => {
if (!selectedDashboard) {
return;
@@ -567,7 +554,9 @@ function NewWidget({
updateDashboardMutation.mutateAsync(dashboard, {
onSuccess: () => {
setToScrollWidgetId(selectedWidget?.id || '');
navigateToDashboardPage();
safeNavigate({
pathname: generatePath(ROUTES.DASHBOARD, { dashboardId }),
});
},
});
}, [
@@ -583,7 +572,7 @@ function NewWidget({
updateDashboardMutation,
widgets,
setToScrollWidgetId,
navigateToDashboardPage,
safeNavigate,
dashboardId,
]);
@@ -592,12 +581,12 @@ function NewWidget({
setDiscardModal(true);
return;
}
navigateToDashboardPage();
}, [isQueryModified, navigateToDashboardPage]);
safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId }));
}, [dashboardId, isQueryModified, safeNavigate]);
const discardChanges = useCallback(() => {
navigateToDashboardPage();
}, [navigateToDashboardPage]);
safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId }));
}, [dashboardId, safeNavigate]);
const setGraphHandler = (type: PANEL_TYPES): void => {
setIsLoadingPanelData(true);
@@ -739,14 +728,12 @@ function NewWidget({
}
const widgetId = query.get('widgetId') || '';
const graphType = query.get('graphType') || '';
const variables = query.get(QueryParams.variables) || '';
const queryParams = {
[QueryParams.expandedWidgetId]: widgetId,
[QueryParams.graphType]: graphType,
[QueryParams.compositeQuery]: encodeURIComponent(
JSON.stringify(currentQuery),
),
[QueryParams.variables]: variables,
};
const updatedSearch = createQueryParams(queryParams);
@@ -835,54 +822,56 @@ function NewWidget({
</LeftContainerWrapper>
<RightContainerWrapper>
<RightContainer
setGraphHandler={setGraphHandler}
title={title}
setTitle={setTitle}
description={description}
setDescription={setDescription}
stackedBarChart={stackedBarChart}
setStackedBarChart={setStackedBarChart}
opacity={opacity}
yAxisUnit={yAxisUnit}
columnUnits={columnUnits}
setColumnUnits={setColumnUnits}
bucketCount={bucketCount}
bucketWidth={bucketWidth}
combineHistogram={combineHistogram}
setCombineHistogram={setCombineHistogram}
setBucketWidth={setBucketWidth}
setBucketCount={setBucketCount}
setOpacity={setOpacity}
selectedNullZeroValue={selectedNullZeroValue}
setSelectedNullZeroValue={setSelectedNullZeroValue}
selectedGraph={graphType}
setSelectedTime={setSelectedTime}
selectedTime={selectedTime}
setYAxisUnit={setYAxisUnit}
decimalPrecision={decimalPrecision}
setDecimalPrecision={setDecimalPrecision}
thresholds={thresholds}
setThresholds={setThresholds}
selectedWidget={selectedWidget}
isFillSpans={isFillSpans}
setIsFillSpans={setIsFillSpans}
isLogScale={isLogScale}
setIsLogScale={setIsLogScale}
legendPosition={legendPosition}
setLegendPosition={setLegendPosition}
customLegendColors={customLegendColors}
setCustomLegendColors={setCustomLegendColors}
queryResponse={queryResponse}
softMin={softMin}
setSoftMin={setSoftMin}
softMax={softMax}
setSoftMax={setSoftMax}
contextLinks={contextLinks}
setContextLinks={setContextLinks}
enableDrillDown={enableDrillDown}
isNewDashboard={isNewDashboard}
/>
<OverlayScrollbar>
<RightContainer
setGraphHandler={setGraphHandler}
title={title}
setTitle={setTitle}
description={description}
setDescription={setDescription}
stackedBarChart={stackedBarChart}
setStackedBarChart={setStackedBarChart}
opacity={opacity}
yAxisUnit={yAxisUnit}
columnUnits={columnUnits}
setColumnUnits={setColumnUnits}
bucketCount={bucketCount}
bucketWidth={bucketWidth}
combineHistogram={combineHistogram}
setCombineHistogram={setCombineHistogram}
setBucketWidth={setBucketWidth}
setBucketCount={setBucketCount}
setOpacity={setOpacity}
selectedNullZeroValue={selectedNullZeroValue}
setSelectedNullZeroValue={setSelectedNullZeroValue}
selectedGraph={graphType}
setSelectedTime={setSelectedTime}
selectedTime={selectedTime}
setYAxisUnit={setYAxisUnit}
decimalPrecision={decimalPrecision}
setDecimalPrecision={setDecimalPrecision}
thresholds={thresholds}
setThresholds={setThresholds}
selectedWidget={selectedWidget}
isFillSpans={isFillSpans}
setIsFillSpans={setIsFillSpans}
isLogScale={isLogScale}
setIsLogScale={setIsLogScale}
legendPosition={legendPosition}
setLegendPosition={setLegendPosition}
customLegendColors={customLegendColors}
setCustomLegendColors={setCustomLegendColors}
queryResponse={queryResponse}
softMin={softMin}
setSoftMin={setSoftMin}
softMax={softMax}
setSoftMax={setSoftMax}
contextLinks={contextLinks}
setContextLinks={setContextLinks}
enableDrillDown={enableDrillDown}
isNewDashboard={isNewDashboard}
/>
</OverlayScrollbar>
</RightContainerWrapper>
</PanelContainer>
<Modal

View File

@@ -15,14 +15,7 @@ export const RightContainerWrapper = styled(Col)`
overflow-y: auto;
}
&::-webkit-scrollbar {
width: 0.3rem;
}
&::-webkit-scrollbar-thumb {
background: rgb(136, 136, 136);
border-radius: 0.625rem;
}
&::-webkit-scrollbar-track {
background: transparent;
width: 0rem;
}
`;

View File

@@ -11,8 +11,11 @@ import { getYAxisCategories } from 'components/YAxisUnitSelector/utils';
import {
initialQueryBuilderFormValuesMap,
PANEL_TYPES,
PANEL_TYPES_INITIAL_QUERY,
} from 'constants/queryBuilder';
import {
listViewInitialLogQuery,
PANEL_TYPES_INITIAL_QUERY,
} from 'container/DashboardContainer/ComponentsSlider/constants';
import {
defaultLogsSelectedColumns,
defaultTraceSelectedColumns,
@@ -546,7 +549,10 @@ export const getDefaultWidgetData = (
nullZeroValues: '',
opacity: '',
panelTypes: name,
query: PANEL_TYPES_INITIAL_QUERY[name],
query:
name === PANEL_TYPES.LIST
? listViewInitialLogQuery
: PANEL_TYPES_INITIAL_QUERY[name],
timePreferance: 'GLOBAL_TIME',
softMax: null,
softMin: null,

View File

@@ -1,339 +0,0 @@
import { renderHook } from '@testing-library/react';
import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashboardFromLocalStorage';
import { useTransformDashboardVariables } from 'hooks/dashboard/useTransformDashboardVariables';
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
jest.mock('hooks/dashboard/useDashboardFromLocalStorage');
jest.mock('hooks/dashboard/useVariablesFromUrl');
const mockUseDashboardVariablesFromLocalStorage = useDashboardVariablesFromLocalStorage as jest.MockedFunction<
typeof useDashboardVariablesFromLocalStorage
>;
const mockUseVariablesFromUrl = useVariablesFromUrl as jest.MockedFunction<
typeof useVariablesFromUrl
>;
const makeVariable = (
overrides: Partial<IDashboardVariable> = {},
): IDashboardVariable => ({
id: 'existing-id',
name: 'env',
description: '',
type: 'QUERY',
sort: 'DISABLED',
multiSelect: false,
showALLOption: false,
selectedValue: 'prod',
...overrides,
});
const makeDashboard = (
variables: Record<string, IDashboardVariable>,
): Dashboard => ({
id: 'dash-1',
createdAt: '',
updatedAt: '',
createdBy: '',
updatedBy: '',
data: {
title: 'Test',
variables,
},
});
const setupHook = (
currentDashboard: Record<string, any> = {},
urlVariables: Record<string, any> = {},
): ReturnType<typeof useTransformDashboardVariables> => {
mockUseDashboardVariablesFromLocalStorage.mockReturnValue({
currentDashboard,
updateLocalStorageDashboardVariables: jest.fn(),
});
mockUseVariablesFromUrl.mockReturnValue({
getUrlVariables: () => urlVariables,
setUrlVariables: jest.fn(),
updateUrlVariable: jest.fn(),
});
const { result } = renderHook(() => useTransformDashboardVariables('dash-1'));
return result.current;
};
describe('useTransformDashboardVariables', () => {
beforeEach(() => jest.clearAllMocks());
describe('order assignment', () => {
it('assigns order starting from 0 to variables that have none', () => {
const { transformDashboardVariables } = setupHook();
const dashboard = makeDashboard({
v1: makeVariable({ id: 'id1', name: 'v1', order: undefined }),
v2: makeVariable({ id: 'id2', name: 'v2', order: undefined }),
});
const result = transformDashboardVariables(dashboard);
const orders = Object.values(result.data.variables).map((v) => v.order);
expect(orders).toContain(0);
expect(orders).toContain(1);
});
it('preserves existing order values', () => {
const { transformDashboardVariables } = setupHook();
const dashboard = makeDashboard({
v1: makeVariable({ id: 'id1', name: 'v1', order: 5 }),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.order).toBe(5);
});
it('assigns unique orders across multiple variables that all lack an order', () => {
const { transformDashboardVariables } = setupHook();
const dashboard = makeDashboard({
v1: makeVariable({ id: 'id1', name: 'v1', order: undefined }),
v2: makeVariable({ id: 'id2', name: 'v2', order: undefined }),
v3: makeVariable({ id: 'id3', name: 'v3', order: undefined }),
});
const result = transformDashboardVariables(dashboard);
const orders = Object.values(result.data.variables).map((v) => v.order);
// All three newly assigned orders must be distinct
expect(new Set(orders).size).toBe(3);
});
});
describe('ID assignment', () => {
it('assigns a UUID to variables that have no id', () => {
const { transformDashboardVariables } = setupHook();
const variable = makeVariable({ name: 'v1' });
(variable as any).id = undefined;
const dashboard = makeDashboard({ v1: variable });
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.id).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
);
});
it('preserves existing IDs', () => {
const { transformDashboardVariables } = setupHook();
const dashboard = makeDashboard({
v1: makeVariable({ id: 'keep-me', name: 'v1' }),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.id).toBe('keep-me');
});
});
describe('TEXTBOX backward compatibility', () => {
it('copies textboxValue to defaultValue when defaultValue is missing', () => {
const { transformDashboardVariables } = setupHook();
const dashboard = makeDashboard({
v1: makeVariable({
id: 'id1',
name: 'v1',
type: 'TEXTBOX',
textboxValue: 'hello',
defaultValue: undefined,
order: undefined,
}),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.defaultValue).toBe('hello');
});
it('does not overwrite an existing defaultValue', () => {
const { transformDashboardVariables } = setupHook();
const dashboard = makeDashboard({
v1: makeVariable({
id: 'id1',
name: 'v1',
type: 'TEXTBOX',
textboxValue: 'old',
defaultValue: 'keep',
order: undefined,
}),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.defaultValue).toBe('keep');
});
});
describe('localStorage merge', () => {
it('applies localStorage selectedValue over DB value', () => {
const { transformDashboardVariables } = setupHook({
env: { selectedValue: 'staging', allSelected: false },
});
const dashboard = makeDashboard({
v1: makeVariable({ id: 'id1', name: 'env', selectedValue: 'prod' }),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.selectedValue).toBe('staging');
});
it('applies localStorage allSelected over DB value', () => {
const { transformDashboardVariables } = setupHook({
env: { selectedValue: undefined, allSelected: true },
});
const dashboard = makeDashboard({
v1: makeVariable({
id: 'id1',
name: 'env',
allSelected: false,
showALLOption: true,
}),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.allSelected).toBe(true);
});
});
describe('URL variable override', () => {
it('sets allSelected=true when URL value is __ALL__', () => {
const { transformDashboardVariables } = setupHook(
{ env: { selectedValue: 'prod', allSelected: false } },
{ env: '__ALL__' },
);
const dashboard = makeDashboard({
v1: makeVariable({
id: 'id1',
name: 'env',
showALLOption: true,
allSelected: false,
}),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.allSelected).toBe(true);
});
it('sets selectedValue from URL and clears allSelected when showALLOption is true', () => {
const { transformDashboardVariables } = setupHook(
{ env: { selectedValue: undefined, allSelected: true } },
{ env: 'dev' },
);
const dashboard = makeDashboard({
v1: makeVariable({
id: 'id1',
name: 'env',
showALLOption: true,
allSelected: true,
multiSelect: false,
}),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.selectedValue).toBe('dev');
expect(result.data.variables.v1.allSelected).toBe(false);
});
it('does not set allSelected=false when showALLOption is false', () => {
const { transformDashboardVariables } = setupHook(
{ env: { selectedValue: undefined, allSelected: true } },
{ env: 'dev' },
);
const dashboard = makeDashboard({
v1: makeVariable({
id: 'id1',
name: 'env',
showALLOption: false,
allSelected: true,
multiSelect: false,
}),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.selectedValue).toBe('dev');
expect(result.data.variables.v1.allSelected).toBe(true);
});
it('normalizes array URL value to single value for single-select variable', () => {
const { transformDashboardVariables } = setupHook(
{},
{ env: ['prod', 'dev'] },
);
const dashboard = makeDashboard({
v1: makeVariable({
id: 'id1',
name: 'env',
multiSelect: false,
}),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.selectedValue).toBe('prod');
});
it('wraps single URL value in array for multi-select variable', () => {
const { transformDashboardVariables } = setupHook({}, { env: 'prod' });
const dashboard = makeDashboard({
v1: makeVariable({
id: 'id1',
name: 'env',
multiSelect: true,
}),
});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.selectedValue).toEqual(['prod']);
});
it('looks up URL variable by variable id when name is absent', () => {
const { transformDashboardVariables } = setupHook(
{},
{ 'var-uuid': 'fallback' },
);
const variable = makeVariable({ id: 'var-uuid', multiSelect: false });
delete variable.name;
const dashboard = makeDashboard({ v1: variable });
const result = transformDashboardVariables(dashboard);
expect(result.data.variables.v1.selectedValue).toBe('fallback');
});
});
describe('edge cases', () => {
it('returns data unchanged when there are no variables', () => {
const { transformDashboardVariables } = setupHook();
const dashboard = makeDashboard({});
const result = transformDashboardVariables(dashboard);
expect(result.data.variables).toEqual({});
});
it('does not mutate the original dashboard', () => {
const { transformDashboardVariables } = setupHook({
env: { selectedValue: 'staging', allSelected: false },
});
const dashboard = makeDashboard({
v1: makeVariable({ id: 'id1', name: 'env', selectedValue: 'prod' }),
});
const originalValue = dashboard.data.variables.v1.selectedValue;
transformDashboardVariables(dashboard);
expect(dashboard.data.variables.v1.selectedValue).toBe(originalValue);
});
});
});

View File

@@ -15,7 +15,7 @@ interface DashboardLocalStorageVariables {
[id: string]: LocalStoreDashboardVariables;
}
export interface UseDashboardVariablesFromLocalStorageReturn {
interface UseDashboardVariablesFromLocalStorageReturn {
currentDashboard: LocalStoreDashboardVariables;
updateLocalStorageDashboardVariables: (
id: string,

View File

@@ -1,128 +0,0 @@
import { ALL_SELECTED_VALUE } from 'components/NewSelect/utils';
import {
useDashboardVariablesFromLocalStorage,
UseDashboardVariablesFromLocalStorageReturn,
} from 'hooks/dashboard/useDashboardFromLocalStorage';
import useVariablesFromUrl, {
UseVariablesFromUrlReturn,
} from 'hooks/dashboard/useVariablesFromUrl';
import { isEmpty } from 'lodash-es';
import { normalizeUrlValueForVariable } from 'providers/Dashboard/normalizeUrlValue';
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
import { v4 as generateUUID } from 'uuid';
export function useTransformDashboardVariables(
dashboardId: string,
): Pick<UseVariablesFromUrlReturn, 'getUrlVariables' | 'updateUrlVariable'> &
UseDashboardVariablesFromLocalStorageReturn & {
transformDashboardVariables: (data: Dashboard) => Dashboard;
} {
const {
currentDashboard,
updateLocalStorageDashboardVariables,
} = useDashboardVariablesFromLocalStorage(dashboardId);
const { getUrlVariables, updateUrlVariable } = useVariablesFromUrl();
const mergeDBWithLocalStorage = (
data: Dashboard,
localStorageVariables: any,
): Dashboard => {
const updatedData = data;
if (data && localStorageVariables) {
const updatedVariables = data.data.variables;
const variablesFromUrl = getUrlVariables();
Object.keys(data.data.variables).forEach((variable) => {
const variableData = data.data.variables[variable];
// values from url
const urlVariable = variableData?.name
? variablesFromUrl[variableData?.name] || variablesFromUrl[variableData.id]
: variablesFromUrl[variableData.id];
let updatedVariable = {
...data.data.variables[variable],
...localStorageVariables[variableData.name as any],
};
// respect the url variable if it is set, override the others
if (!isEmpty(urlVariable)) {
if (urlVariable === ALL_SELECTED_VALUE) {
updatedVariable = {
...updatedVariable,
allSelected: true,
};
} else {
// Normalize URL value to match variable's multiSelect configuration
const normalizedValue = normalizeUrlValueForVariable(
urlVariable,
variableData,
);
updatedVariable = {
...updatedVariable,
selectedValue: normalizedValue,
// Only set allSelected to false if showALLOption is available
...(updatedVariable?.showALLOption && { allSelected: false }),
};
}
}
updatedVariables[variable] = updatedVariable;
});
updatedData.data.variables = updatedVariables;
}
return updatedData;
};
// As we do not have order and ID's in the variables object, we have to process variables to add order and ID if they do not exist in the variables object
// eslint-disable-next-line sonarjs/cognitive-complexity
const transformDashboardVariables = (data: Dashboard): Dashboard => {
if (data && data.data && data.data.variables) {
const clonedDashboardData = mergeDBWithLocalStorage(
JSON.parse(JSON.stringify(data)),
currentDashboard,
);
const { variables } = clonedDashboardData.data;
const existingOrders: Set<number> = new Set();
for (const key in variables) {
// eslint-disable-next-line no-prototype-builtins
if (variables.hasOwnProperty(key)) {
const variable: IDashboardVariable = variables[key];
// Check if 'order' property doesn't exist or is undefined
if (variable.order === undefined) {
// Find a unique order starting from 0
let order = 0;
while (existingOrders.has(order)) {
order += 1;
}
variable.order = order;
existingOrders.add(order);
// ! BWC - Specific case for backward compatibility where textboxValue was used instead of defaultValue
if (variable.type === 'TEXTBOX' && !variable.defaultValue) {
variable.defaultValue = variable.textboxValue || '';
}
}
if (variable.id === undefined) {
variable.id = generateUUID();
}
}
}
return clonedDashboardData;
}
return data;
};
return {
transformDashboardVariables,
getUrlVariables,
updateUrlVariable,
currentDashboard,
updateLocalStorageDashboardVariables,
};
}

View File

@@ -11,7 +11,7 @@ export interface LocalStoreDashboardVariables {
| IDashboardVariable['selectedValue'];
}
export interface UseVariablesFromUrlReturn {
interface UseVariablesFromUrlReturn {
getUrlVariables: () => LocalStoreDashboardVariables;
setUrlVariables: (variables: LocalStoreDashboardVariables) => void;
updateUrlVariable: (

View File

@@ -12,8 +12,6 @@ import {
ATTRIBUTE_TYPES,
initialAutocompleteData,
initialQueryBuilderFormValuesMap,
listViewInitialLogQuery,
listViewInitialTraceQuery,
mapOfFormulaToFilters,
mapOfQueryFilters,
PANEL_TYPES,
@@ -25,6 +23,10 @@ import {
metricsUnknownSpaceAggregateOperatorOptions,
metricsUnknownTimeAggregateOperatorOptions,
} from 'constants/queryBuilderOperators';
import {
listViewInitialLogQuery,
listViewInitialTraceQuery,
} from 'container/DashboardContainer/ComponentsSlider/constants';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { getMetricsOperatorsByAttributeType } from 'lib/newQueryBuilder/getMetricsOperatorsByAttributeType';
import { getOperatorsBySourceAndPanelType } from 'lib/newQueryBuilder/getOperatorsBySourceAndPanelType';

View File

@@ -1,143 +0,0 @@
import { Route } from 'react-router-dom';
import * as getDashboardModule from 'api/v1/dashboards/id/get';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { rest, server } from 'mocks-server/server';
import { render, screen, waitFor } from 'tests/test-utils';
import DashboardWidget from '../index';
const DASHBOARD_ID = 'dash-1';
const WIDGET_ID = 'widget-abc';
const mockDashboardResponse = {
status: 'success',
data: {
id: DASHBOARD_ID,
createdAt: '2024-01-01T00:00:00Z',
createdBy: 'test',
updatedAt: '2024-01-01T00:00:00Z',
updatedBy: 'test',
isLocked: false,
data: {
collapsableRowsMigrated: true,
description: '',
name: '',
panelMap: {},
tags: [],
title: 'Test Dashboard',
uploadedGrafana: false,
uuid: '',
version: '',
variables: {},
widgets: [],
layout: [],
},
},
};
const mockSafeNavigate = jest.fn();
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
safeNavigate: mockSafeNavigate,
}),
}));
jest.mock('container/NewWidget', () => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="new-widget">NewWidget</div>,
}));
// Wrap component in a Route so useParams can resolve dashboardId.
// Query params are passed via the URL so useUrlQuery (react-router) can read them.
function renderAtRoute(
queryState: Record<string, string | null> = {},
): ReturnType<typeof render> {
const params = new URLSearchParams();
Object.entries(queryState).forEach(([k, v]) => {
if (v !== null) {
params.set(k, v);
}
});
const search = params.toString() ? `?${params.toString()}` : '';
return render(
<Route path="/dashboard/:dashboardId/new">
<DashboardWidget />
</Route>,
undefined,
{ initialRoute: `/dashboard/${DASHBOARD_ID}/new${search}` },
);
}
beforeEach(() => {
mockSafeNavigate.mockClear();
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('DashboardWidget', () => {
it('redirects to dashboard when widgetId is missing', async () => {
renderAtRoute({ graphType: PANEL_TYPES.TIME_SERIES });
await waitFor(() => {
expect(mockSafeNavigate).toHaveBeenCalled();
});
const [navigatedTo] = mockSafeNavigate.mock.calls[0];
expect(navigatedTo).toContain(`/dashboard/${DASHBOARD_ID}`);
});
it('redirects to dashboard when graphType is missing', async () => {
renderAtRoute({ widgetId: WIDGET_ID });
await waitFor(() => {
expect(mockSafeNavigate).toHaveBeenCalled();
});
const [navigatedTo] = mockSafeNavigate.mock.calls[0];
expect(navigatedTo).toContain(`/dashboard/${DASHBOARD_ID}`);
});
it('shows spinner while dashboard is loading', () => {
// Spy instead of MSW delay('infinite') to avoid leaving an open network handle.
jest
.spyOn(getDashboardModule, 'default')
.mockReturnValue(new Promise(() => {}));
renderAtRoute({ widgetId: WIDGET_ID, graphType: PANEL_TYPES.TIME_SERIES });
expect(screen.getByRole('img', { name: 'loading' })).toBeInTheDocument();
});
it('shows error message when dashboard fetch fails', async () => {
server.use(
rest.get(
`http://localhost/api/v1/dashboards/${DASHBOARD_ID}`,
(_req, res, ctx) => res(ctx.status(500), ctx.json({ status: 'error' })),
),
);
renderAtRoute({ widgetId: WIDGET_ID, graphType: PANEL_TYPES.TIME_SERIES });
await waitFor(() => {
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
});
});
it('renders NewWidget when dashboard loads successfully', async () => {
server.use(
rest.get(
`http://localhost/api/v1/dashboards/${DASHBOARD_ID}`,
(_req, res, ctx) => res(ctx.status(200), ctx.json(mockDashboardResponse)),
),
);
renderAtRoute({ widgetId: WIDGET_ID, graphType: PANEL_TYPES.TIME_SERIES });
await waitFor(() => {
expect(screen.getByTestId('new-widget')).toBeInTheDocument();
});
});
});

View File

@@ -1,34 +1,29 @@
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo } from 'react';
import { useQuery } from 'react-query';
import { generatePath, useParams } from 'react-router-dom';
import { Card, Typography } from 'antd';
import getDashboard from 'api/v1/dashboards/id/get';
import Spinner from 'components/Spinner';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { DASHBOARD_CACHE_TIME } from 'constants/queryCacheTime';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import ROUTES from 'constants/routes';
import NewWidget from 'container/NewWidget';
import { isDrilldownEnabled } from 'container/QueryTable/Drilldown/drilldownUtils';
import { useTransformDashboardVariables } from 'hooks/dashboard/useTransformDashboardVariables';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { parseAsStringEnum, useQueryState } from 'nuqs';
import { setDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
import { Dashboard } from 'types/api/dashboard/getAll';
function DashboardWidget(): JSX.Element | null {
const { dashboardId } = useParams<{
dashboardId: string;
}>();
const query = useUrlQuery();
const { graphType, widgetId } = useMemo(() => {
return {
graphType: query.get(QueryParams.graphType) as PANEL_TYPES,
widgetId: query.get(QueryParams.widgetId),
};
}, [query]);
const [widgetId] = useQueryState('widgetId');
const [graphType] = useQueryState(
'graphType',
parseAsStringEnum<PANEL_TYPES>(Object.values(PANEL_TYPES)),
);
const { safeNavigate } = useSafeNavigate();
@@ -62,15 +57,8 @@ function DashboardWidgetInternal({
widgetId: string;
graphType: PANEL_TYPES;
}): JSX.Element | null {
const [selectedDashboard, setSelectedDashboard] = useState<
Dashboard | undefined
>(undefined);
const { transformDashboardVariables } = useTransformDashboardVariables(
dashboardId,
);
const {
data: dashboardResponse,
isFetching: isFetchingDashboardResponse,
isError: isErrorDashboardResponse,
} = useQuery([REACT_QUERY_KEY.DASHBOARD_BY_ID, dashboardId, widgetId], {
@@ -82,15 +70,17 @@ function DashboardWidgetInternal({
refetchOnWindowFocus: false,
cacheTime: DASHBOARD_CACHE_TIME,
onSuccess: (response) => {
const updatedDashboardData = transformDashboardVariables(response.data);
setSelectedDashboard(updatedDashboardData);
setDashboardVariablesStore({
dashboardId,
variables: updatedDashboardData.data.variables,
variables: response.data.data.variables,
});
},
});
const selectedDashboard = useMemo(() => dashboardResponse?.data, [
dashboardResponse?.data,
]);
if (isFetchingDashboardResponse) {
return <Spinner tip="Loading.." />;
}

View File

@@ -17,18 +17,21 @@ import { useDispatch, useSelector } from 'react-redux';
import { Modal } from 'antd';
import getDashboard from 'api/v1/dashboards/id/get';
import locked from 'api/v1/dashboards/id/lock';
import { ALL_SELECTED_VALUE } from 'components/NewSelect/utils';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import dayjs, { Dayjs } from 'dayjs';
import { useTransformDashboardVariables } from 'hooks/dashboard/useTransformDashboardVariables';
import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashboardFromLocalStorage';
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
import useTabVisibility from 'hooks/useTabFocus';
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
import { defaultTo } from 'lodash-es';
import { defaultTo, isEmpty } from 'lodash-es';
import isEqual from 'lodash-es/isEqual';
import isUndefined from 'lodash-es/isUndefined';
import omitBy from 'lodash-es/omitBy';
import { useAppContext } from 'providers/App/App';
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
import { normalizeUrlValueForVariable } from 'providers/Dashboard/normalizeUrlValue';
import { useErrorModal } from 'providers/ErrorModalProvider';
// eslint-disable-next-line no-restricted-imports
import { Dispatch } from 'redux';
@@ -36,9 +39,10 @@ import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
import { SuccessResponseV2 } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll';
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as generateUUID } from 'uuid';
import {
DASHBOARD_CACHE_TIME,
@@ -53,7 +57,9 @@ import { IDashboardContext, WidgetColumnWidths } from './types';
import { sortLayout } from './util';
export const DashboardContext = createContext<IDashboardContext>({
isDashboardSliderOpen: false,
isDashboardLocked: false,
handleToggleDashboardSlider: () => {},
handleDashboardLockToggle: () => {},
dashboardResponse: {} as UseQueryResult<
SuccessResponseV2<Dashboard>,
@@ -80,6 +86,8 @@ export function DashboardProvider({
children,
dashboardId,
}: PropsWithChildren<{ dashboardId: string }>): JSX.Element {
const [isDashboardSliderOpen, setIsDashboardSlider] = useState<boolean>(false);
const [isDashboardLocked, setIsDashboardLocked] = useState<boolean>(false);
const [
@@ -129,10 +137,9 @@ export function DashboardProvider({
const {
currentDashboard,
updateLocalStorageDashboardVariables,
getUrlVariables,
updateUrlVariable,
transformDashboardVariables,
} = useTransformDashboardVariables(dashboardId);
} = useDashboardVariablesFromLocalStorage(dashboardId);
const { getUrlVariables, updateUrlVariable } = useVariablesFromUrl();
const updatedTimeRef = useRef<Dayjs | null>(null); // Using ref to store the updated time
const modalRef = useRef<any>(null);
@@ -144,6 +151,99 @@ export function DashboardProvider({
const [isDashboardFetching, setIsDashboardFetching] = useState<boolean>(false);
const mergeDBWithLocalStorage = (
data: Dashboard,
localStorageVariables: any,
): Dashboard => {
const updatedData = data;
if (data && localStorageVariables) {
const updatedVariables = data.data.variables;
const variablesFromUrl = getUrlVariables();
Object.keys(data.data.variables).forEach((variable) => {
const variableData = data.data.variables[variable];
// values from url
const urlVariable = variableData?.name
? variablesFromUrl[variableData?.name] || variablesFromUrl[variableData.id]
: variablesFromUrl[variableData.id];
let updatedVariable = {
...data.data.variables[variable],
...localStorageVariables[variableData.name as any],
};
// respect the url variable if it is set, override the others
if (!isEmpty(urlVariable)) {
if (urlVariable === ALL_SELECTED_VALUE) {
updatedVariable = {
...updatedVariable,
allSelected: true,
};
} else {
// Normalize URL value to match variable's multiSelect configuration
const normalizedValue = normalizeUrlValueForVariable(
urlVariable,
variableData,
);
updatedVariable = {
...updatedVariable,
selectedValue: normalizedValue,
// Only set allSelected to false if showALLOption is available
...(updatedVariable?.showALLOption && { allSelected: false }),
};
}
}
updatedVariables[variable] = updatedVariable;
});
updatedData.data.variables = updatedVariables;
}
return updatedData;
};
// As we do not have order and ID's in the variables object, we have to process variables to add order and ID if they do not exist in the variables object
// eslint-disable-next-line sonarjs/cognitive-complexity
const transformDashboardVariables = (data: Dashboard): Dashboard => {
if (data && data.data && data.data.variables) {
const clonedDashboardData = mergeDBWithLocalStorage(
JSON.parse(JSON.stringify(data)),
currentDashboard,
);
const { variables } = clonedDashboardData.data;
const existingOrders: Set<number> = new Set();
for (const key in variables) {
// eslint-disable-next-line no-prototype-builtins
if (variables.hasOwnProperty(key)) {
const variable: IDashboardVariable = variables[key];
// Check if 'order' property doesn't exist or is undefined
if (variable.order === undefined) {
// Find a unique order starting from 0
let order = 0;
while (existingOrders.has(order)) {
order += 1;
}
variable.order = order;
existingOrders.add(order);
// ! BWC - Specific case for backward compatibility where textboxValue was used instead of defaultValue
if (variable.type === 'TEXTBOX' && !variable.defaultValue) {
variable.defaultValue = variable.textboxValue || '';
}
}
if (variable.id === undefined) {
variable.id = generateUUID();
}
}
}
return clonedDashboardData;
}
return data;
};
const dashboardResponse = useQuery(
[
REACT_QUERY_KEY.DASHBOARD_BY_ID,
@@ -174,14 +274,13 @@ export function DashboardProvider({
},
onSuccess: (data: SuccessResponseV2<Dashboard>) => {
const updatedDashboardData = transformDashboardVariables(data?.data);
// initialize URL variables after dashboard state is set to avoid race conditions
const variables = updatedDashboardData?.data?.variables;
// if the url variable is not set for any variable, set it to the default value
const variables = data?.data?.data?.variables;
if (variables) {
initializeDefaultVariables(variables, getUrlVariables, updateUrlVariable);
}
const updatedDashboardData = transformDashboardVariables(data?.data);
const updatedDate = dayjs(updatedDashboardData?.updatedAt);
setIsDashboardLocked(updatedDashboardData?.locked || false);
@@ -284,8 +383,13 @@ export function DashboardProvider({
}
}, [isVisible]);
const handleToggleDashboardSlider = (value: boolean): void => {
setIsDashboardSlider(value);
};
const { mutate: lockDashboard } = useMutation(locked, {
onSuccess: (_, props) => {
setIsDashboardSlider(false);
setIsDashboardLocked(props.lock);
},
onError: (error) => {
@@ -310,7 +414,9 @@ export function DashboardProvider({
const value: IDashboardContext = useMemo(
() => ({
isDashboardSliderOpen,
isDashboardLocked,
handleToggleDashboardSlider,
handleDashboardLockToggle,
dashboardResponse,
selectedDashboard,
@@ -330,6 +436,7 @@ export function DashboardProvider({
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[
isDashboardSliderOpen,
isDashboardLocked,
dashboardResponse,
selectedDashboard,

View File

@@ -381,7 +381,6 @@ describe('Dashboard Provider - URL Variables Integration', () => {
multiSelect: false,
allSelected: false,
showALLOption: true,
order: 0,
},
services: {
id: 'svc-id',
@@ -389,7 +388,6 @@ describe('Dashboard Provider - URL Variables Integration', () => {
multiSelect: true,
allSelected: false,
showALLOption: true,
order: 1,
},
},
mockGetUrlVariables,

View File

@@ -1,18 +0,0 @@
import { create } from 'zustand';
interface IPanelTypeSelectionModalState {
isPanelTypeSelectionModalOpen: boolean;
setIsPanelTypeSelectionModalOpen: (isOpen: boolean) => void;
}
/**
* This helper is used for selecting the panel type when creating a new panel in the dashboard.
* It uses Zustand for state management to keep track of whether the panel type selection modal is open or closed.
*/
export const usePanelTypeSelectionModalStore = create<IPanelTypeSelectionModalState>(
(set) => ({
isPanelTypeSelectionModalOpen: false,
setIsPanelTypeSelectionModalOpen: (isOpen): void =>
set({ isPanelTypeSelectionModalOpen: isOpen }),
}),
);

View File

@@ -9,7 +9,9 @@ export type WidgetColumnWidths = {
};
export interface IDashboardContext {
isDashboardSliderOpen: boolean;
isDashboardLocked: boolean;
handleToggleDashboardSlider: (value: boolean) => void;
handleDashboardLockToggle: (value: boolean) => void;
dashboardResponse: UseQueryResult<SuccessResponseV2<Dashboard>, unknown>;
selectedDashboard: Dashboard | undefined;

View File

@@ -1,70 +0,0 @@
package common
// Signal is the telemetry signal type for a builder query.
#Signal: "metrics" | "logs" | "traces"
// QueryName is a valid identifier for a query (e.g., "A", "B1", "my_query").
#QueryName: =~"^[A-Za-z][A-Za-z0-9_]*$"
// ReduceTo specifies how a multi-series result is reduced to a single value.
#ReduceTo: "sum" | "count" | "avg" | "min" | "max" | "last" | "median"
// Limit constrains the maximum number of result rows.
#Limit: int & >=0 & <=10000
// PageSize constrains the number of rows per page.
#PageSize: int & >=1
// Offset is a non-negative row offset for pagination.
#Offset: int & >=0
// VariableSortOrder controls how variable values are sorted.
#VariableSortOrder: *"DISABLED" | "ASC" | "DESC"
// MetricAggregation defines a structured aggregation for metrics queries.
#MetricAggregation: close({
metricName: string & !=""
timeAggregation: "latest" | "sum" | "avg" | "min" | "max" | "count" | "rate" | "increase"
spaceAggregation: "sum" | "avg" | "min" | "max" | "count" | "p50" | "p75" | "p90" | "p95" | "p99"
reduceTo?: #ReduceTo
temporality?: "delta" | "cumulative" | "unspecified"
})
// ExpressionAggregation defines an expression-based aggregation for logs/traces queries.
#ExpressionAggregation: close({
expression: string & !=""
alias?: string
})
// FilterExpression is a filter condition string.
#FilterExpression: close({
expression: string
})
// GroupByItem specifies a grouping column.
#GroupByItem: close({
name: string & !=""
fieldDataType?: string
fieldContext?: string
})
// OrderByItem specifies a column ordering.
#OrderByItem: close({
columnName: string & !=""
order: "asc" | "desc"
})
// HavingExpression is a post-aggregation filter.
#HavingExpression: close({
expression: string
})
// Function is a post-query transformation.
#Function: close({
name: "cutOffMin" | "cutOffMax" | "clampMin" | "clampMax" |
"absolute" | "runningDiff" | "log2" | "log10" |
"cumulativeSum" | "ewma3" | "ewma5" | "ewma7" |
"median3" | "median5" | "median7" | "timeShift" |
"anomaly" | "fillZero"
args?: [...close({value: number | string | bool})]
})

View File

@@ -1,4 +0,0 @@
module: "github.com/signoz/signoz/schemas"
language: {
version: "v0.12.0"
}

View File

@@ -1,9 +0,0 @@
{
"id": "signoz",
"name": "signoz",
"metaData": {
"buildInfo": {
"buildVersion": "0.0.1"
}
}
}

View File

@@ -1,118 +0,0 @@
{
"name": "@signoz/perses-plugin",
"version": "0.0.1",
"_comment_panels": "TimeSeriesChart is the only panel type used currently. Other Perses built-in panels like StatChart, GaugeChart, BarChart, PieChart, Table, etc. can be added as needed.",
"_comment_queries": "Only TimeSeriesQuery is used currently since the example dashboard only has time series queries. LogQuery, TraceQuery, and ProfileQuery kinds will also be needed.",
"perses": {
"plugins": [
{
"kind": "Panel",
"spec": {
"name": "TimeSeriesChart",
"display": {
"name": "Time Series Chart"
}
}
},
{
"kind": "TimeSeriesQuery",
"spec": {
"name": "SigNozBuilderQuery",
"display": {
"name": "SigNoz Builder Query"
}
}
},
{
"kind": "TimeSeriesQuery",
"spec": {
"name": "SigNozFormula",
"display": {
"name": "SigNoz Formula"
}
}
},
{
"kind": "TimeSeriesQuery",
"spec": {
"name": "SigNozJoin",
"display": {
"name": "SigNoz Join"
}
}
},
{
"kind": "TimeSeriesQuery",
"spec": {
"name": "SigNozTraceOperator",
"display": {
"name": "SigNoz Trace Operator"
}
}
},
{
"kind": "TimeSeriesQuery",
"spec": {
"name": "SigNozCompositeQuery",
"display": {
"name": "SigNoz Composite Query"
}
}
},
{
"kind": "TimeSeriesQuery",
"spec": {
"name": "SigNozPromQL",
"display": {
"name": "SigNoz PromQL"
}
}
},
{
"kind": "TimeSeriesQuery",
"spec": {
"name": "SigNozClickHouseSQL",
"display": {
"name": "SigNoz ClickHouse SQL"
}
}
},
{
"kind": "Variable",
"spec": {
"name": "SigNozQueryVariable",
"display": {
"name": "SigNoz Query Variable"
}
}
},
{
"kind": "Variable",
"spec": {
"name": "SigNozCustomVariable",
"display": {
"name": "SigNoz Custom Variable"
}
}
},
{
"kind": "Variable",
"spec": {
"name": "SigNozDynamicVariable",
"display": {
"name": "SigNoz Dynamic Variable"
}
}
},
{
"kind": "Datasource",
"spec": {
"name": "SigNozDatasource",
"display": {
"name": "SigNoz Datasource"
}
}
}
]
}
}

View File

@@ -1,27 +0,0 @@
Panel Plugins
All SigNoz panels use Perses built-in panel kinds directly. No custom CUE
schemas exist yet because Perses does not publish CUE schemas for its panel
plugins (only Go SDK + TypeScript). Once it does, each panel can embed the
upstream spec and add the SigNoz-specific fields listed below.
SigNoz-specific fields by panel type:
TimeSeriesChart timePreference
StatChart timePreference, contextLinks
BarChart timePreference, contextLinks
PieChart timePreference, contextLinks
Table timePreference, contextLinks
LogsTable (List) timePreference, selectedLogFields, selectedTracesFields, columnWidths
TraceTable timePreference, selectedTracesFields, columnWidths
HistogramChart timePreference, contextLinks, bucketCount, bucketWidth, mergeAllActiveQueries
Common fields:
timePreference — panel-local vs dashboard-global time range
contextLinks — clickable drill-down links on data points
Panel-specific fields:
selectedLogFields / selectedTracesFields — which fields to display as columns in list views
columnWidths — saved column width overrides
bucketCount / bucketWidth — histogram bucket configuration
mergeAllActiveQueries — combine multiple queries into one histogram

View File

@@ -1,37 +0,0 @@
package model
import "github.com/signoz/signoz/schemas/common"
// Source: pkg/types/querybuildertypes/querybuildertypesv5/builder_query.go — QueryBuilderQuery
kind: "SigNozBuilderQuery"
spec: close({
name: common.#QueryName
signal: common.#Signal
expression: string
disabled?: bool | *false
// Metrics use structured aggregations; logs/traces use expression-based.
aggregations?: [...common.#MetricAggregation]
expressionAggregations?: [...common.#ExpressionAggregation]
filter?: common.#FilterExpression
groupBy?: [...common.#GroupByItem]
order?: [...common.#OrderByItem]
selectFields?: [...]
limit?: common.#Limit
limitBy?: #LimitBy
offset?: common.#Offset
cursor?: string
having?: common.#HavingExpression
// secondaryAggregations not added — not yet implemented.
functions?: [...common.#Function]
legend?: string
stepInterval?: number
reduceTo?: common.#ReduceTo
pageSize?: common.#PageSize
source?: string
})
#LimitBy: close({
keys: [...string]
value: string
})

View File

@@ -1,24 +0,0 @@
{
"kind": "SigNozBuilderQuery",
"spec": {
"name": "A",
"signal": "metrics",
"expression": "A",
"aggregations": [
{
"metricName": "redis_keyspace_hits",
"timeAggregation": "rate",
"spaceAggregation": "sum",
"reduceTo": "sum"
}
],
"filter": {
"expression": "host_name IN $host_name"
},
"groupBy": [],
"order": [],
"disabled": false,
"legend": "Hit/s across all hosts",
"stepInterval": 60
}
}

View File

@@ -1,12 +0,0 @@
package model
import "github.com/signoz/signoz/schemas/common"
// Source: pkg/types/querybuildertypes/querybuildertypesv5/clickhouse_query.go — ClickHouseQuery
kind: "SigNozClickHouseSQL"
spec: close({
name: common.#QueryName
query: string & !=""
disabled?: bool | *false
legend?: string
})

View File

@@ -1,9 +0,0 @@
{
"kind": "SigNozClickHouseSQL",
"spec": {
"name": "A",
"query": "SELECT toStartOfInterval(timestamp, INTERVAL 1 MINUTE) AS ts, count() AS total FROM signoz_logs.distributed_logs GROUP BY ts ORDER BY ts",
"disabled": false,
"legend": "Log count"
}
}

View File

@@ -1,24 +0,0 @@
package model
// Source: pkg/types/querybuildertypes/querybuildertypesv5/req.go — CompositeQuery
// SigNozCompositeQuery groups multiple query plugins into a single
// query request. Each entry is a typed envelope whose spec is
// validated by the corresponding plugin schema.
kind: "SigNozCompositeQuery"
spec: close({
queries: [...#QueryEnvelope]
})
// QueryEnvelope wraps a single query plugin with a type discriminator.
#QueryEnvelope: close({
type: #QueryType
spec: {...}
})
#QueryType:
"builder_query" |
"builder_formula" |
"builder_join" |
"builder_trace_operator" |
"promql" |
"clickhouse_sql"

View File

@@ -1,53 +0,0 @@
{
"kind": "SigNozCompositeQuery",
"spec": {
"queries": [
{
"type": "builder_query",
"spec": {
"name": "A",
"signal": "metrics",
"expression": "A",
"aggregations": [
{
"metricName": "redis_keyspace_hits",
"timeAggregation": "rate",
"spaceAggregation": "sum",
"reduceTo": "sum"
}
],
"filter": {
"expression": "host_name IN $host_name"
}
}
},
{
"type": "builder_query",
"spec": {
"name": "B",
"signal": "metrics",
"expression": "B",
"aggregations": [
{
"metricName": "redis_keyspace_misses",
"timeAggregation": "rate",
"spaceAggregation": "sum",
"reduceTo": "sum"
}
],
"filter": {
"expression": "host_name IN $host_name"
}
}
},
{
"type": "builder_formula",
"spec": {
"name": "F1",
"expression": "A / (A + B) * 100",
"legend": "Hit rate %"
}
}
]
}
}

View File

@@ -1,12 +0,0 @@
package model
import "github.com/signoz/signoz/schemas/common"
// defaultValue lives on the Perses ListVariable wrapper (spec level).
kind: "SigNozCustomVariable"
spec: close({
customValue: string
sort?: common.#VariableSortOrder
multiSelect?: bool
showALLOption?: bool
})

View File

@@ -1,8 +0,0 @@
{
"kind": "SigNozCustomVariable",
"spec": {
"customValue": "production,staging,development",
"sort": "DISABLED",
"multiSelect": false
}
}

View File

@@ -1,9 +0,0 @@
package model
kind: "SigNozDatasource"
// SigNoz has a single built-in backend — the frontend already knows
// the API endpoint, so there is no connection config to validate.
// Add fields here if SigNoz ever supports multiple backends or
// configurable API versions.
spec: close({})

View File

@@ -1,4 +0,0 @@
{
"kind": "SigNozDatasource",
"spec": {}
}

View File

@@ -1,13 +0,0 @@
package model
import "github.com/signoz/signoz/schemas/common"
// defaultValue lives on the Perses ListVariable wrapper (spec level).
kind: "SigNozDynamicVariable"
spec: close({
dynamicVariablesAttribute: string
dynamicVariablesSource: string
sort?: common.#VariableSortOrder
multiSelect?: bool
showALLOption?: bool
})

View File

@@ -1,10 +0,0 @@
{
"kind": "SigNozDynamicVariable",
"spec": {
"dynamicVariablesAttribute": "host_name",
"dynamicVariablesSource": "metrics",
"sort": "ASC",
"multiSelect": true,
"showALLOption": true
}
}

View File

@@ -1,16 +0,0 @@
package model
import "github.com/signoz/signoz/schemas/common"
// Source: pkg/types/querybuildertypes/querybuildertypesv5/formula.go — QueryBuilderFormula
kind: "SigNozFormula"
spec: close({
name: common.#QueryName
expression: string
disabled?: bool | *false
legend?: string
limit?: common.#Limit
having?: common.#HavingExpression
stepInterval?: number
order?: [...common.#OrderByItem]
})

View File

@@ -1,8 +0,0 @@
{
"kind": "SigNozFormula",
"spec": {
"name": "F1",
"expression": "A / B * 100",
"legend": "Hit rate %"
}
}

View File

@@ -1,30 +0,0 @@
package model
import "github.com/signoz/signoz/schemas/common"
// Source: pkg/types/querybuildertypes/querybuildertypesv5/join.go — QueryBuilderJoin
kind: "SigNozJoin"
spec: close({
name: common.#QueryName
left: #QueryRef
right: #QueryRef
type: #JoinType
on: string
disabled?: bool | *false
aggregations?: [...common.#MetricAggregation]
expressionAggregations?: [...common.#ExpressionAggregation]
selectFields?: [...]
filter?: common.#FilterExpression
groupBy?: [...common.#GroupByItem]
having?: common.#HavingExpression
// secondaryAggregations not added — not yet implemented.
order?: [...common.#OrderByItem]
limit?: common.#Limit
functions?: [...common.#Function]
})
#QueryRef: close({
name: common.#QueryName
})
#JoinType: "inner" | "left" | "right" | "full" | "cross"

View File

@@ -1,11 +0,0 @@
{
"kind": "SigNozJoin",
"spec": {
"name": "J1",
"left": {"name": "A"},
"right": {"name": "B"},
"type": "inner",
"on": "service.name = service.name",
"disabled": false
}
}

View File

@@ -1,14 +0,0 @@
package model
import "github.com/signoz/signoz/schemas/common"
// Source: pkg/types/querybuildertypes/querybuildertypesv5/prom_query.go — PromQuery
kind: "SigNozPromQL"
spec: close({
name: common.#QueryName
query: string & !=""
disabled?: bool | *false
step?: number
stats?: bool
legend?: string
})

View File

@@ -1,9 +0,0 @@
{
"kind": "SigNozPromQL",
"spec": {
"name": "A",
"query": "rate(http_requests_total{status=\"200\"}[5m])",
"disabled": false,
"legend": "{{method}} {{path}}"
}
}

View File

@@ -1,12 +0,0 @@
package model
import "github.com/signoz/signoz/schemas/common"
// defaultValue lives on the Perses ListVariable wrapper (spec level).
kind: "SigNozQueryVariable"
spec: close({
queryValue: string
sort?: common.#VariableSortOrder
multiSelect?: bool
showALLOption?: bool
})

View File

@@ -1,9 +0,0 @@
{
"kind": "SigNozQueryVariable",
"spec": {
"queryValue": "SELECT DISTINCT host_name FROM signoz_metrics.distributed_time_series_v4_1day WHERE metric_name = 'redis_cpu_time'",
"sort": "ASC",
"multiSelect": true,
"showALLOption": true
}
}

View File

@@ -1,31 +0,0 @@
package model
import "github.com/signoz/signoz/schemas/common"
// Source: pkg/types/querybuildertypes/querybuildertypesv5/trace_operator.go — QueryBuilderTraceOperator
// SigNozTraceOperator composes multiple trace BuilderQueries using
// relational operators (=>, ->, &&, ||, NOT) to query trace relationships.
// Signal is implicitly "traces" — all referenced queries must be trace queries.
kind: "SigNozTraceOperator"
spec: close({
name: common.#QueryName
// Operator expression composing trace queries, e.g. "A => B && C".
expression: string & !=""
disabled?: bool | *false
// Which query's spans to return (must be a query referenced in expression).
returnSpansFrom?: common.#QueryName
aggregations?: [...common.#ExpressionAggregation]
filter?: common.#FilterExpression
groupBy?: [...common.#GroupByItem]
order?: [...common.#OrderByItem]
limit?: common.#Limit
offset?: common.#Offset
cursor?: string
functions?: [...common.#Function]
stepInterval?: number
having?: common.#HavingExpression
legend?: string
selectFields?: [...]
})

View File

@@ -1,19 +0,0 @@
{
"kind": "SigNozTraceOperator",
"spec": {
"name": "T1",
"expression": "A => B",
"returnSpansFrom": "A",
"aggregations": [
{
"expression": "count()",
"alias": "request_count"
}
],
"filter": {
"expression": "service.name = 'frontend'"
},
"groupBy": [],
"order": []
}
}

View File

@@ -1,10 +0,0 @@
package model
// Stub for the Perses built-in TimeSeriesChart panel plugin.
// percli --plugin.path does not load built-in schemas, so we provide an
// open spec here to let panel validation pass. Replace with the real
// schema from https://github.com/perses/plugins/tree/main/timeserieschart
// when strict panel validation is needed.
kind: "TimeSeriesChart"
spec: {...}

View File

@@ -1,185 +0,0 @@
{
"description": "This dashboard shows the Redis instance overview. It includes latency, hit/miss rate, connections, and memory information.\n",
"id": "redis-overview",
"layout": [
{
"h": 3,
"i": "a77227c7-16f5-4353-952e-b183c715a61c",
"moved": false,
"static": false,
"w": 6,
"x": 0,
"y": 0
},
{
"h": 3,
"i": "bf0deeeb-e926-4234-944c-82bacd96af47",
"moved": false,
"static": false,
"w": 6,
"x": 6,
"y": 0
}
],
"tags": [
"redis",
"database"
],
"title": "Redis overview",
"variables": {
"94f19b3c-ad9f-4b47-a9b2-f312c09fa965": {
"allSelected": true,
"customValue": "",
"description": "List of hosts sending Redis metrics",
"id": "94f19b3c-ad9f-4b47-a9b2-f312c09fa965",
"key": "94f19b3c-ad9f-4b47-a9b2-f312c09fa965",
"modificationUUID": "4c5b0c03-9cbc-425b-8d8e-7152e5c39ba8",
"multiSelect": true,
"name": "host_name",
"order": 0,
"queryValue": "SELECT JSONExtractString(labels, 'host_name') AS host_name\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'redis_cpu_time'\nGROUP BY host_name",
"selectedValue": [
"Srikanths-MacBook-Pro.local"
],
"showALLOption": true,
"sort": "ASC",
"textboxValue": "",
"type": "QUERY"
}
},
"version": "v5",
"widgets": [
{
"description": "Rate successful lookup of keys in the main dictionary",
"fillSpans": false,
"id": "a77227c7-16f5-4353-952e-b183c715a61c",
"isStacked": false,
"nullZeroValues": "zero",
"opacity": "1",
"panelTypes": "graph",
"query": {
"builder": {
"queryData": [
{
"aggregations": [
{
"metricName": "redis_keyspace_hits",
"reduceTo": "sum",
"spaceAggregation": "sum",
"temporality": null,
"timeAggregation": "rate"
}
],
"dataSource": "metrics",
"disabled": false,
"expression": "A",
"filter": {
"expression": "host_name IN $host_name"
},
"groupBy": [],
"having": {
"expression": ""
},
"legend": "Hit/s across all hosts",
"limit": null,
"orderBy": [],
"queryName": "A",
"stepInterval": 60
}
],
"queryFormulas": []
},
"clickhouse_sql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"id": "42c9c117-bfaf-49f7-b528-aad099392295",
"promql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"queryType": "builder"
},
"softMax": null,
"softMin": null,
"thresholds": [],
"timePreferance": "GLOBAL_TIME",
"title": "Hits/s",
"yAxisUnit": "none"
},
{
"description": "Number of clients pending on a blocking call",
"fillSpans": false,
"id": "bf0deeeb-e926-4234-944c-82bacd96af47",
"isStacked": false,
"nullZeroValues": "zero",
"opacity": "1",
"panelTypes": "graph",
"query": {
"builder": {
"queryData": [
{
"aggregations": [
{
"metricName": "redis_clients_blocked",
"reduceTo": "sum",
"spaceAggregation": "sum",
"temporality": null,
"timeAggregation": "sum"
}
],
"dataSource": "metrics",
"disabled": false,
"expression": "A",
"filter": {
"expression": "host_name IN $host_name"
},
"groupBy": [],
"having": {
"expression": ""
},
"legend": "Blocked clients across all hosts",
"limit": null,
"orderBy": [],
"queryName": "A",
"stepInterval": 60
}
],
"queryFormulas": []
},
"clickhouse_sql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"id": "6b48fc1c-014a-45e6-a4ce-2c0e06a23302",
"promql": [
{
"disabled": false,
"legend": "",
"name": "A",
"query": ""
}
],
"queryType": "builder"
},
"softMax": null,
"softMin": null,
"thresholds": [],
"timePreferance": "GLOBAL_TIME",
"title": "Blocked clients",
"yAxisUnit": "none"
}
]
}

View File

@@ -1,160 +0,0 @@
{
"kind": "Dashboard",
"metadata": {
"name": "redis-overview",
"project": "signoz"
},
"spec": {
"display": {
"name": "Redis overview",
"description": "This dashboard shows the Redis instance overview. It includes latency, hit/miss rate, connections, and memory information."
},
"duration": "1h",
"variables": [
{
"kind": "ListVariable",
"spec": {
"name": "host_name",
"display": {
"name": "host_name",
"description": "List of hosts sending Redis metrics"
},
"allowAllValue": true,
"allowMultiple": true,
"plugin": {
"kind": "SigNozQueryVariable",
"spec": {
"queryValue": "SELECT JSONExtractString(labels, 'host_name') AS host_name\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'redis_cpu_time'\nGROUP BY host_name",
"sort": "ASC",
"multiSelect": true,
"showALLOption": true
}
}
}
}
],
"panels": {
"a77227c7": {
"kind": "Panel",
"spec": {
"display": {
"name": "Hits/s",
"description": "Rate successful lookup of keys in the main dictionary"
},
"plugin": {
"kind": "TimeSeriesChart",
"spec": {}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "SigNozBuilderQuery",
"spec": {
"name": "A",
"signal": "metrics",
"aggregations": [
{
"metricName": "redis_keyspace_hits",
"reduceTo": "sum",
"spaceAggregation": "sum",
"timeAggregation": "rate"
}
],
"filter": {
"expression": "host_name IN $host_name"
},
"groupBy": [],
"order": [],
"expression": "A",
"disabled": false,
"legend": "Hit/s across all hosts",
"stepInterval": 60,
"having": {
"expression": ""
}
}
}
}
}
]
}
},
"bf0deeeb": {
"kind": "Panel",
"spec": {
"display": {
"name": "Blocked clients",
"description": "Number of clients pending on a blocking call"
},
"plugin": {
"kind": "TimeSeriesChart",
"spec": {}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "SigNozBuilderQuery",
"spec": {
"name": "A",
"signal": "metrics",
"aggregations": [
{
"metricName": "redis_clients_blocked",
"reduceTo": "sum",
"spaceAggregation": "sum",
"timeAggregation": "sum"
}
],
"filter": {
"expression": "host_name IN $host_name"
},
"groupBy": [],
"order": [],
"expression": "A",
"disabled": false,
"legend": "Blocked clients across all hosts",
"stepInterval": 60,
"having": {
"expression": ""
}
}
}
}
}
]
}
}
},
"layouts": [
{
"kind": "Grid",
"spec": {
"items": [
{
"x": 0,
"y": 0,
"width": 6,
"height": 3,
"content": {
"$ref": "#/spec/panels/a77227c7"
}
},
{
"x": 6,
"y": 0,
"width": 6,
"height": 3,
"content": {
"$ref": "#/spec/panels/bf0deeeb"
}
}
]
}
}
]
}
}

View File

@@ -1,8 +0,0 @@
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
percli lint -f "$SCRIPT_DIR/examples/redis-overview-perses.json" --plugin.path "$SCRIPT_DIR" --log.level fatal
# percli lint auto-generates this file as a side effect; clean it up.
rm -f "$SCRIPT_DIR/plugin-modules.json"

View File

@@ -1,41 +0,0 @@
package loghandler
import (
"context"
"log/slog"
"github.com/SigNoz/signoz/pkg/errors"
)
type filtering struct{}
func NewFiltering() *filtering {
return &filtering{}
}
func (h *filtering) Wrap(next LogHandler) LogHandler {
return LogHandlerFunc(func(ctx context.Context, record slog.Record) error {
if !filterRecord(record) {
return nil
}
return next.Handle(ctx, record)
})
}
func filterRecord(record slog.Record) bool {
suppress := false
record.Attrs(func(a slog.Attr) bool {
if a.Value.Kind() == slog.KindAny {
if err, ok := a.Value.Any().(error); ok {
if errors.Is(err, context.Canceled) {
suppress = true
return false
}
}
}
return true
})
return !suppress
}

View File

@@ -1,52 +0,0 @@
package loghandler
import (
"bytes"
"context"
"encoding/json"
"log/slog"
"testing"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFiltering_SuppressesContextCanceled(t *testing.T) {
filtering := NewFiltering()
buf := bytes.NewBuffer(nil)
logger := slog.New(&handler{base: slog.NewJSONHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}), wrappers: []Wrapper{filtering}})
logger.ErrorContext(context.Background(), "operation failed", "error", context.Canceled)
assert.Empty(t, buf.String(), "log with context.Canceled should be suppressed")
}
func TestFiltering_AllowsOtherErrors(t *testing.T) {
filtering := NewFiltering()
buf := bytes.NewBuffer(nil)
logger := slog.New(&handler{base: slog.NewJSONHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}), wrappers: []Wrapper{filtering}})
logger.ErrorContext(context.Background(), "operation failed", "error", errors.New(errors.TypeInternal, errors.CodeInternal, "some other error"))
m := make(map[string]any)
err := json.Unmarshal(buf.Bytes(), &m)
require.NoError(t, err)
assert.Equal(t, "operation failed", m["msg"])
}
func TestFiltering_AllowsLogsWithoutErrors(t *testing.T) {
filtering := NewFiltering()
buf := bytes.NewBuffer(nil)
logger := slog.New(&handler{base: slog.NewJSONHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}), wrappers: []Wrapper{filtering}})
logger.InfoContext(context.Background(), "normal log", "key", "value")
m := make(map[string]any)
err := json.Unmarshal(buf.Bytes(), &m)
require.NoError(t, err)
assert.Equal(t, "normal log", m["msg"])
}

View File

@@ -116,7 +116,7 @@ func New(ctx context.Context, cfg Config, build version.Build, serviceName strin
meterProvider: meterProvider,
meterProviderShutdownFunc: meterProviderShutdownFunc,
prometheusRegistry: prometheusRegistry,
logger: NewLogger(cfg, loghandler.NewCorrelation(), loghandler.NewFiltering()),
logger: NewLogger(cfg, loghandler.NewCorrelation()),
startCh: make(chan struct{}),
}, nil
}

View File

@@ -7,14 +7,13 @@ import (
"strings"
"time"
"log/slog"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/opamptypes"
"github.com/SigNoz/signoz/pkg/valuer"
"go.uber.org/zap"
"golang.org/x/exp/slices"
)
@@ -122,7 +121,7 @@ func (r *Repo) insertConfig(
// allowing empty elements for logs - use case is deleting all pipelines
if len(elements) == 0 && c.ElementType != opamptypes.ElementTypeLogPipelines {
slog.ErrorContext(ctx, "insert config called with no elements", "element_type", c.ElementType.StringValue())
zap.L().Error("insert config called with no elements ", zap.String("ElementType", c.ElementType.StringValue()))
return errors.NewInvalidInputf(CodeConfigElementsRequired, "config must have atleast one element")
}
@@ -130,13 +129,13 @@ func (r *Repo) insertConfig(
// the version can not be set by the user, we want to auto-assign the versions
// in a monotonically increasing order starting with 1. hence, we reject insert
// requests with version anything other than 0. here, 0 indicates un-assigned
slog.ErrorContext(ctx, "invalid version assignment while inserting agent config", "version", c.Version, "element_type", c.ElementType.StringValue())
zap.L().Error("invalid version assignment while inserting agent config", zap.Int("version", c.Version), zap.String("ElementType", c.ElementType.StringValue()))
return errors.NewInvalidInputf(errors.CodeInvalidInput, "user defined versions are not supported in the agent config")
}
configVersion, err := r.GetLatestVersion(ctx, orgId, c.ElementType)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
slog.ErrorContext(ctx, "failed to fetch latest config version", "error", err)
zap.L().Error("failed to fetch latest config version", zap.Error(err))
return err
}
@@ -156,11 +155,11 @@ func (r *Repo) insertConfig(
// Delete elements first, then version (to respect potential foreign key constraints)
_, delErr := r.store.BunDB().NewDelete().Model(new(opamptypes.AgentConfigElement)).Where("version_id = ?", c.ID).Exec(ctx)
if delErr != nil {
slog.ErrorContext(ctx, "failed to delete config elements during cleanup", "error", delErr, "version_id", c.ID.String())
zap.L().Error("failed to delete config elements during cleanup", zap.Error(delErr), zap.String("version_id", c.ID.String()))
}
_, delErr = r.store.BunDB().NewDelete().Model(new(opamptypes.AgentConfigVersion)).Where("id = ?", c.ID).Where("org_id = ?", orgId).Exec(ctx)
if delErr != nil {
slog.ErrorContext(ctx, "failed to delete config version during cleanup", "error", delErr, "version_id", c.ID.String())
zap.L().Error("failed to delete config version during cleanup", zap.Error(delErr), zap.String("version_id", c.ID.String()))
}
}
}()
@@ -171,7 +170,7 @@ func (r *Repo) insertConfig(
Model(c).
Exec(ctx)
if dbErr != nil {
slog.ErrorContext(ctx, "error in inserting config version", "error", dbErr)
zap.L().Error("error in inserting config version: ", zap.Error(dbErr))
return errors.WrapInternalf(dbErr, CodeConfigVersionInsertFailed, "failed to insert config version")
}
@@ -222,7 +221,7 @@ func (r *Repo) updateDeployStatus(ctx context.Context,
Where("org_id = ?", orgId).
Exec(ctx)
if err != nil {
slog.ErrorContext(ctx, "failed to update deploy status", "error", err)
zap.L().Error("failed to update deploy status", zap.Error(err))
return model.BadRequest(fmt.Errorf("failed to update deploy status"))
}
@@ -240,7 +239,7 @@ func (r *Repo) updateDeployStatusByHash(
Where("org_id = ?", orgId).
Exec(ctx)
if err != nil {
slog.ErrorContext(ctx, "failed to update deploy status", "error", err)
zap.L().Error("failed to update deploy status", zap.Error(err))
return errors.WrapInternalf(err, CodeConfigDeployStatusUpdateFailed, "failed to update deploy status")
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"crypto/sha256"
"fmt"
"log/slog"
"strings"
"sync"
"sync/atomic"
@@ -18,6 +17,7 @@ import (
"github.com/SigNoz/signoz/pkg/types/opamptypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/google/uuid"
"go.uber.org/zap"
yaml "gopkg.in/yaml.v3"
)
@@ -36,8 +36,7 @@ type AgentFeatureType string
type Manager struct {
Repo
// lock to make sure only one update is sent to remote agents at a time
lock uint32
logger *slog.Logger
lock uint32
// For AgentConfigProvider implementation
agentFeatures []AgentFeature
@@ -68,7 +67,6 @@ func Initiate(options *ManagerOptions) (*Manager, error) {
m = &Manager{
Repo: Repo{options.Store},
logger: slog.Default(),
agentFeatures: options.AgentFeatures,
configSubscribers: map[string]func(){},
}
@@ -224,19 +222,19 @@ func NotifyConfigUpdate(ctx context.Context) {
func Redeploy(ctx context.Context, orgId valuer.UUID, typ opamptypes.ElementType, version int) error {
configVersion, err := GetConfigVersion(ctx, orgId, typ, version)
if err != nil {
slog.ErrorContext(ctx, "failed to fetch config version during redeploy", "error", err)
zap.L().Error("failed to fetch config version during redeploy", zap.Error(err))
return err
}
if configVersion == nil || (configVersion != nil && configVersion.Config == "") {
slog.DebugContext(ctx, "config version has no conf yaml", "config_version", configVersion)
zap.L().Debug("config version has no conf yaml", zap.Any("configVersion", configVersion))
return errors.NewInvalidInputf(CodeConfigVersionNoConfig, "the config version can not be redeployed")
}
switch typ {
case opamptypes.ElementTypeSamplingRules:
var config *tsp.Config
if err := yaml.Unmarshal([]byte(configVersion.Config), &config); err != nil {
slog.DebugContext(ctx, "failed to read last conf correctly", "error", err)
zap.L().Debug("failed to read last conf correctly", zap.Error(err))
return model.BadRequest(fmt.Errorf("failed to read the stored config correctly"))
}
@@ -248,7 +246,7 @@ func Redeploy(ctx context.Context, orgId valuer.UUID, typ opamptypes.ElementType
opamp.AddToTracePipelineSpec("signoz_tail_sampling")
configHash, err := opamp.UpsertControlProcessors(ctx, "traces", processorConf, m.OnConfigUpdate)
if err != nil {
slog.ErrorContext(ctx, "failed to call agent config update for trace processor", "error", err)
zap.L().Error("failed to call agent config update for trace processor", zap.Error(err))
return errors.WithAdditionalf(err, "failed to deploy the config")
}
@@ -256,7 +254,7 @@ func Redeploy(ctx context.Context, orgId valuer.UUID, typ opamptypes.ElementType
case opamptypes.ElementTypeDropRules:
var filterConfig *filterprocessor.Config
if err := yaml.Unmarshal([]byte(configVersion.Config), &filterConfig); err != nil {
slog.ErrorContext(ctx, "failed to read last conf correctly", "error", err)
zap.L().Error("failed to read last conf correctly", zap.Error(err))
return model.InternalError(fmt.Errorf("failed to read the stored config correctly"))
}
processorConf := map[string]interface{}{
@@ -266,7 +264,7 @@ func Redeploy(ctx context.Context, orgId valuer.UUID, typ opamptypes.ElementType
opamp.AddToMetricsPipelineSpec("filter")
configHash, err := opamp.UpsertControlProcessors(ctx, "metrics", processorConf, m.OnConfigUpdate)
if err != nil {
slog.ErrorContext(ctx, "failed to call agent config update for trace processor", "error", err)
zap.L().Error("failed to call agent config update for trace processor", zap.Error(err))
return err
}
@@ -292,13 +290,13 @@ func UpsertFilterProcessor(ctx context.Context, orgId valuer.UUID, version int,
opamp.AddToMetricsPipelineSpec("filter")
configHash, err := opamp.UpsertControlProcessors(ctx, "metrics", processorConf, m.OnConfigUpdate)
if err != nil {
slog.ErrorContext(ctx, "failed to call agent config update for trace processor", "error", err)
zap.L().Error("failed to call agent config update for trace processor", zap.Error(err))
return err
}
processorConfYaml, yamlErr := yaml.Marshal(config)
if yamlErr != nil {
slog.WarnContext(ctx, "unexpected error while transforming processor config to yaml", "error", yamlErr)
zap.L().Warn("unexpected error while transforming processor config to yaml", zap.Error(yamlErr))
}
m.updateDeployStatus(ctx, orgId, opamptypes.ElementTypeDropRules, version, opamptypes.DeployInitiated.StringValue(), "Deployment started", configHash, string(processorConfYaml))
@@ -317,7 +315,7 @@ func (m *Manager) OnConfigUpdate(orgId valuer.UUID, agentId string, hash string,
message := "Deployment was successful"
defer func() {
m.logger.Info(status, "agent_id", agentId, "agent_response", message)
zap.L().Info(status, zap.String("agentId", agentId), zap.String("agentResponse", message))
}()
if err != nil {
@@ -343,13 +341,13 @@ func UpsertSamplingProcessor(ctx context.Context, orgId valuer.UUID, version int
opamp.AddToTracePipelineSpec("signoz_tail_sampling")
configHash, err := opamp.UpsertControlProcessors(ctx, "traces", processorConf, m.OnConfigUpdate)
if err != nil {
slog.ErrorContext(ctx, "failed to call agent config update for trace processor", "error", err)
zap.L().Error("failed to call agent config update for trace processor", zap.Error(err))
return err
}
processorConfYaml, yamlErr := yaml.Marshal(config)
if yamlErr != nil {
slog.WarnContext(ctx, "unexpected error while transforming processor config to yaml", "error", yamlErr)
zap.L().Warn("unexpected error while transforming processor config to yaml", zap.Error(yamlErr))
}
m.updateDeployStatus(ctx, orgId, opamptypes.ElementTypeSamplingRules, version, opamptypes.DeployInitiated.StringValue(), "Deployment started", configHash, string(processorConfYaml))

View File

@@ -11,6 +11,7 @@ import (
"github.com/SigNoz/signoz-otel-collector/utils/fingerprint"
"github.com/SigNoz/signoz/pkg/query-service/model"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"go.uber.org/zap"
)
func (r *ClickHouseReader) GetQBFilterSuggestionsForLogs(
@@ -78,7 +79,7 @@ func (r *ClickHouseReader) GetQBFilterSuggestionsForLogs(
)
if err != nil {
// Do not fail the entire request if only example query generation fails
r.logger.ErrorContext(ctx, "could not find attribute values for creating example query", "error", err)
zap.L().Error("could not find attribute values for creating example query", zap.Error(err))
} else {
// add example queries for as many attributes as possible.
@@ -158,7 +159,10 @@ func (r *ClickHouseReader) getValuesForLogAttributes(
*/
if len(attributes) > 10 {
r.logger.ErrorContext(ctx, "log attribute values requested for too many attributes. This can lead to slow and costly queries", "count", len(attributes))
zap.L().Error(
"log attribute values requested for too many attributes. This can lead to slow and costly queries",
zap.Int("count", len(attributes)),
)
attributes = attributes[:10]
}
@@ -183,7 +187,7 @@ func (r *ClickHouseReader) getValuesForLogAttributes(
rows, err := r.db.Query(ctx, query, tagKeyQueryArgs...)
if err != nil {
r.logger.ErrorContext(ctx, "couldn't query attrib values for suggestions", "error", err)
zap.L().Error("couldn't query attrib values for suggestions", zap.Error(err))
return nil, model.InternalError(fmt.Errorf(
"couldn't query attrib values for suggestions: %w", err,
))

View File

@@ -2,18 +2,17 @@ package queryprogress
import (
"fmt"
"log/slog"
"sync"
"github.com/ClickHouse/clickhouse-go/v2"
"github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/google/uuid"
"go.uber.org/zap"
"golang.org/x/exp/maps"
)
// tracks progress and manages subscriptions for all queries
type inMemoryQueryProgressTracker struct {
logger *slog.Logger
queries map[string]*queryTracker
lock sync.RWMutex
}
@@ -31,7 +30,7 @@ func (tracker *inMemoryQueryProgressTracker) ReportQueryStarted(
))
}
tracker.queries[queryId] = newQueryTracker(tracker.logger, queryId)
tracker.queries[queryId] = newQueryTracker(queryId)
return func() {
tracker.onQueryFinished(queryId)
@@ -94,7 +93,6 @@ func (tracker *inMemoryQueryProgressTracker) getQueryTracker(
// Tracks progress and manages subscriptions for a single query
type queryTracker struct {
logger *slog.Logger
queryId string
isFinished bool
@@ -104,9 +102,8 @@ type queryTracker struct {
lock sync.Mutex
}
func newQueryTracker(logger *slog.Logger, queryId string) *queryTracker {
func newQueryTracker(queryId string) *queryTracker {
return &queryTracker{
logger: logger,
queryId: queryId,
subscriptions: map[string]*queryProgressSubscription{},
}
@@ -117,7 +114,10 @@ func (qt *queryTracker) handleProgressUpdate(p *clickhouse.Progress) {
defer qt.lock.Unlock()
if qt.isFinished {
qt.logger.Warn("received clickhouse progress update for finished query", "queryId", qt.queryId, "progress", p)
zap.L().Warn(
"received clickhouse progress update for finished query",
zap.String("queryId", qt.queryId), zap.Any("progress", p),
)
return
}
@@ -146,7 +146,7 @@ func (qt *queryTracker) subscribe() (
}
subscriberId := uuid.NewString()
subscription := newQueryProgressSubscription(qt.logger)
subscription := newQueryProgressSubscription()
qt.subscriptions[subscriberId] = subscription
if qt.progress != nil {
@@ -163,7 +163,11 @@ func (qt *queryTracker) unsubscribe(subscriberId string) {
defer qt.lock.Unlock()
if qt.isFinished {
qt.logger.Debug("received unsubscribe request after query finished", "subscriber", subscriberId, "queryId", qt.queryId)
zap.L().Debug(
"received unsubscribe request after query finished",
zap.String("subscriber", subscriberId),
zap.String("queryId", qt.queryId),
)
return
}
@@ -179,7 +183,10 @@ func (qt *queryTracker) onFinished() {
defer qt.lock.Unlock()
if qt.isFinished {
qt.logger.Warn("receiver query finish report after query finished", "queryId", qt.queryId)
zap.L().Warn(
"receiver query finish report after query finished",
zap.String("queryId", qt.queryId),
)
return
}
@@ -192,17 +199,15 @@ func (qt *queryTracker) onFinished() {
}
type queryProgressSubscription struct {
logger *slog.Logger
ch chan model.QueryProgress
isClosed bool
lock sync.Mutex
}
func newQueryProgressSubscription(logger *slog.Logger) *queryProgressSubscription {
func newQueryProgressSubscription() *queryProgressSubscription {
ch := make(chan model.QueryProgress, 1000)
return &queryProgressSubscription{
logger: logger,
ch: ch,
ch: ch,
}
}
@@ -212,7 +217,10 @@ func (ch *queryProgressSubscription) send(progress model.QueryProgress) {
defer ch.lock.Unlock()
if ch.isClosed {
ch.logger.Error("can't send query progress: channel already closed.", "progress", progress)
zap.L().Error(
"can't send query progress: channel already closed.",
zap.Any("progress", progress),
)
return
}
@@ -220,9 +228,12 @@ func (ch *queryProgressSubscription) send(progress model.QueryProgress) {
// blocking while sending doesn't happen in the happy path
select {
case ch.ch <- progress:
ch.logger.Debug("published query progress", "progress", progress)
zap.L().Debug("published query progress", zap.Any("progress", progress))
default:
ch.logger.Error("couldn't publish query progress. dropping update.", "progress", progress)
zap.L().Error(
"couldn't publish query progress. dropping update.",
zap.Any("progress", progress),
)
}
}

View File

@@ -1,8 +1,6 @@
package queryprogress
import (
"log/slog"
"github.com/ClickHouse/clickhouse-go/v2"
"github.com/SigNoz/signoz/pkg/query-service/model"
)
@@ -23,11 +21,10 @@ type QueryProgressTracker interface {
SubscribeToQueryProgress(queryId string) (ch <-chan model.QueryProgress, unsubscribe func(), apiErr *model.ApiError)
}
func NewQueryProgressTracker(logger *slog.Logger) QueryProgressTracker {
func NewQueryProgressTracker() QueryProgressTracker {
// InMemory tracker is useful only for single replica query service setups.
// Multi replica setups must use a centralized store for tracking and subscribing to query progress
return &inMemoryQueryProgressTracker{
logger: logger,
queries: map[string]*queryTracker{},
}
}

View File

@@ -1,7 +1,6 @@
package queryprogress
import (
"log/slog"
"testing"
"time"
@@ -13,7 +12,7 @@ import (
func TestQueryProgressTracking(t *testing.T) {
require := require.New(t)
tracker := NewQueryProgressTracker(slog.Default())
tracker := NewQueryProgressTracker()
testQueryId := "test-query"

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,6 @@ import (
"github.com/SigNoz/signoz/pkg/queryparser"
"io"
"log/slog"
"math"
"net/http"
"regexp"
@@ -74,6 +73,8 @@ import (
"github.com/SigNoz/signoz/pkg/types/ruletypes"
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
"go.uber.org/zap"
"github.com/SigNoz/signoz/pkg/query-service/app/integrations/messagingQueues/kafka"
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
@@ -96,7 +97,6 @@ func NewRouter() *mux.Router {
// APIHandler implements the query service public API
type APIHandler struct {
logger *slog.Logger
reader interfaces.Reader
ruleManager *rules.Manager
querier interfaces.Querier
@@ -212,7 +212,6 @@ func NewAPIHandler(opts APIHandlerOpts, config signoz.Config) (*APIHandler, erro
//quickFilterModule := quickfilter.NewAPI(opts.QuickFilterModule)
aH := &APIHandler{
logger: slog.Default(),
reader: opts.Reader,
temporalityMap: make(map[string]map[v3.Temporality]bool),
ruleManager: opts.RuleManager,
@@ -252,13 +251,13 @@ func NewAPIHandler(opts APIHandlerOpts, config signoz.Config) (*APIHandler, erro
// TODO(nitya): remote this in later for multitenancy.
orgs, err := opts.Signoz.Modules.OrgGetter.ListByOwnedKeyRange(context.Background())
if err != nil {
aH.logger.Warn("unexpected error while fetching orgs while initializing base api handler", "error", err)
zap.L().Warn("unexpected error while fetching orgs while initializing base api handler", zap.Error(err))
}
// if the first org with the first user is created then the setup is complete.
if len(orgs) == 1 {
count, err := opts.Signoz.Modules.UserGetter.CountByOrgID(context.Background(), orgs[0].ID)
if err != nil {
aH.logger.Warn("unexpected error while fetching user count while initializing base api handler", "error", err)
zap.L().Warn("unexpected error while fetch user count while initializing base api handler", zap.Error(err))
}
if count > 0 {
@@ -313,7 +312,7 @@ func RespondError(w http.ResponseWriter, apiErr model.BaseApiError, data interfa
Data: data,
})
if err != nil {
slog.Error("error marshalling json response", "error", err)
zap.L().Error("error marshalling json response", zap.Error(err))
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -345,7 +344,7 @@ func RespondError(w http.ResponseWriter, apiErr model.BaseApiError, data interfa
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
if n, err := w.Write(b); err != nil {
slog.Error("error writing response", "bytes_written", n, "error", err)
zap.L().Error("error writing response", zap.Int("bytesWritten", n), zap.Error(err))
}
}
@@ -357,7 +356,7 @@ func writeHttpResponse(w http.ResponseWriter, data interface{}) {
Data: data,
})
if err != nil {
slog.Error("error marshalling json response", "error", err)
zap.L().Error("error marshalling json response", zap.Error(err))
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -365,7 +364,7 @@ func writeHttpResponse(w http.ResponseWriter, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if n, err := w.Write(b); err != nil {
slog.Error("error writing response", "bytes_written", n, "error", err)
zap.L().Error("error writing response", zap.Int("bytesWritten", n), zap.Error(err))
}
}
@@ -937,14 +936,14 @@ func (aH *APIHandler) metaForLinks(ctx context.Context, rule *ruletypes.Gettable
}
keys = model.GetLogFieldsV3(ctx, params, logFields)
} else {
aH.logger.ErrorContext(ctx, "failed to get log fields using empty keys", "error", apiErr)
zap.L().Error("failed to get log fields using empty keys; the link might not work as expected", zap.Error(apiErr))
}
} else if rule.AlertType == ruletypes.AlertTypeTraces {
traceFields, err := aH.reader.GetSpanAttributeKeysByNames(ctx, logsv3.GetFieldNames(rule.PostableRule.RuleCondition.CompositeQuery))
if err == nil {
keys = traceFields
} else {
aH.logger.ErrorContext(ctx, "failed to get span attributes using empty keys", "error", err)
zap.L().Error("failed to get span attributes using empty keys; the link might not work as expected", zap.Error(err))
}
}
@@ -1277,14 +1276,14 @@ func (aH *APIHandler) List(rw http.ResponseWriter, r *http.Request) {
installedIntegrationDashboards, apiErr := aH.IntegrationsController.GetDashboardsForInstalledIntegrations(ctx, orgID)
if apiErr != nil {
aH.logger.ErrorContext(ctx, "failed to get dashboards for installed integrations", "error", apiErr)
zap.L().Error("failed to get dashboards for installed integrations", zap.Error(apiErr))
} else {
dashboards = append(dashboards, installedIntegrationDashboards...)
}
cloudIntegrationDashboards, apiErr := aH.CloudIntegrationsController.AvailableDashboards(ctx, orgID)
if apiErr != nil {
aH.logger.ErrorContext(ctx, "failed to get dashboards for cloud integrations", "error", apiErr)
zap.L().Error("failed to get dashboards for cloud integrations", zap.Error(apiErr))
} else {
dashboards = append(dashboards, cloudIntegrationDashboards...)
}
@@ -1326,7 +1325,7 @@ func (aH *APIHandler) testRule(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
body, err := io.ReadAll(r.Body)
if err != nil {
aH.logger.ErrorContext(r.Context(), "error reading request body for test rule", "error", err)
zap.L().Error("Error in getting req body in test rule API", zap.Error(err))
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
return
}
@@ -1378,7 +1377,7 @@ func (aH *APIHandler) patchRule(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
body, err := io.ReadAll(r.Body)
if err != nil {
aH.logger.ErrorContext(r.Context(), "error reading request body for patch rule", "error", err)
zap.L().Error("error in getting req body of patch rule API\n", zap.Error(err))
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
return
}
@@ -1408,7 +1407,7 @@ func (aH *APIHandler) editRule(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
body, err := io.ReadAll(r.Body)
if err != nil {
aH.logger.ErrorContext(r.Context(), "error reading request body for edit rule", "error", err)
zap.L().Error("error in getting req body of edit rule API", zap.Error(err))
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
return
}
@@ -1433,7 +1432,7 @@ func (aH *APIHandler) createRule(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
body, err := io.ReadAll(r.Body)
if err != nil {
aH.logger.ErrorContext(r.Context(), "error reading request body for create rule", "error", err)
zap.L().Error("Error in getting req body for create rule API", zap.Error(err))
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
return
}
@@ -1457,7 +1456,7 @@ func (aH *APIHandler) queryRangeMetrics(w http.ResponseWriter, r *http.Request)
return
}
// TODO: add structured logging for query and apiError if needed
// zap.L().Info(query, apiError)
ctx := r.Context()
if to := r.FormValue("timeout"); to != "" {
@@ -1479,7 +1478,7 @@ func (aH *APIHandler) queryRangeMetrics(w http.ResponseWriter, r *http.Request)
}
if res.Err != nil {
aH.logger.ErrorContext(r.Context(), "error in query range metrics", "error", res.Err)
zap.L().Error("error in query range metrics", zap.Error(res.Err))
}
if res.Err != nil {
@@ -1512,7 +1511,7 @@ func (aH *APIHandler) queryMetrics(w http.ResponseWriter, r *http.Request) {
return
}
// TODO: add structured logging for query and apiError if needed
// zap.L().Info(query, apiError)
ctx := r.Context()
if to := r.FormValue("timeout"); to != "" {
@@ -1534,7 +1533,7 @@ func (aH *APIHandler) queryMetrics(w http.ResponseWriter, r *http.Request) {
}
if res.Err != nil {
aH.logger.ErrorContext(r.Context(), "error in query range metrics", "error", res.Err)
zap.L().Error("error in query range metrics", zap.Error(res.Err))
}
if res.Err != nil {
@@ -1637,7 +1636,7 @@ func (aH *APIHandler) getServicesTopLevelOps(w http.ResponseWriter, r *http.Requ
var params topLevelOpsParams
err := json.NewDecoder(r.Body).Decode(&params)
if err != nil {
aH.logger.ErrorContext(r.Context(), "error reading request body for get top operations", "error", err)
zap.L().Error("Error in getting req body for get top operations API", zap.Error(err))
}
if params.Service != "" {
@@ -2059,7 +2058,7 @@ func (aH *APIHandler) HandleError(w http.ResponseWriter, err error, statusCode i
return false
}
if statusCode == http.StatusInternalServerError {
aH.logger.Error("internal server error in http handler", "error", err)
zap.L().Error("HTTP handler, Internal Server Error", zap.Error(err))
}
structuredResp := structuredResponse{
Errors: []structuredError{
@@ -2153,7 +2152,7 @@ func (aH *APIHandler) onboardProducers(
) {
messagingQueue, apiErr := ParseKafkaQueueBody(r)
if apiErr != nil {
aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", "error", apiErr.Err)
zap.L().Error(apiErr.Err.Error())
RespondError(w, apiErr, nil)
return
}
@@ -2161,7 +2160,7 @@ func (aH *APIHandler) onboardProducers(
chq, err := kafka.BuildClickHouseQuery(messagingQueue, kafka.KafkaQueue, "onboard_producers")
if err != nil {
aH.logger.ErrorContext(r.Context(), "failed to build clickhouse query for onboard producers", "error", err)
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
@@ -2255,7 +2254,7 @@ func (aH *APIHandler) onboardConsumers(
) {
messagingQueue, apiErr := ParseKafkaQueueBody(r)
if apiErr != nil {
aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", "error", apiErr.Err)
zap.L().Error(apiErr.Err.Error())
RespondError(w, apiErr, nil)
return
}
@@ -2263,7 +2262,7 @@ func (aH *APIHandler) onboardConsumers(
chq, err := kafka.BuildClickHouseQuery(messagingQueue, kafka.KafkaQueue, "onboard_consumers")
if err != nil {
aH.logger.ErrorContext(r.Context(), "failed to build clickhouse query for onboard consumers", "error", err)
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
@@ -2402,7 +2401,7 @@ func (aH *APIHandler) onboardKafka(w http.ResponseWriter, r *http.Request) {
messagingQueue, apiErr := ParseKafkaQueueBody(r)
if apiErr != nil {
aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", "error", apiErr.Err)
zap.L().Error(apiErr.Err.Error())
RespondError(w, apiErr, nil)
return
}
@@ -2410,7 +2409,7 @@ func (aH *APIHandler) onboardKafka(w http.ResponseWriter, r *http.Request) {
queryRangeParams, err := kafka.BuildBuilderQueriesKafkaOnboarding(messagingQueue)
if err != nil {
aH.logger.ErrorContext(r.Context(), "failed to build kafka onboarding queries", "error", err)
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
@@ -2512,19 +2511,19 @@ func (aH *APIHandler) getNetworkData(w http.ResponseWriter, r *http.Request) {
messagingQueue, apiErr := ParseKafkaQueueBody(r)
if apiErr != nil {
aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", "error", apiErr.Err)
zap.L().Error(apiErr.Err.Error())
RespondError(w, apiErr, nil)
return
}
queryRangeParams, err := kafka.BuildQRParamsWithCache(messagingQueue, "throughput", attributeCache)
if err != nil {
aH.logger.ErrorContext(r.Context(), "failed to build query range params for throughput", "error", err)
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
aH.logger.ErrorContext(r.Context(), "failed to validate query range params", "error", err)
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
@@ -2563,12 +2562,12 @@ func (aH *APIHandler) getNetworkData(w http.ResponseWriter, r *http.Request) {
queryRangeParams, err = kafka.BuildQRParamsWithCache(messagingQueue, "fetch-latency", attributeCache)
if err != nil {
aH.logger.ErrorContext(r.Context(), "failed to build query range params for fetch latency", "error", err)
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
aH.logger.ErrorContext(r.Context(), "failed to validate query range params for fetch latency", "error", err)
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
@@ -2623,7 +2622,7 @@ func (aH *APIHandler) getProducerData(w http.ResponseWriter, r *http.Request) {
messagingQueue, apiErr := ParseKafkaQueueBody(r)
if apiErr != nil {
aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", "error", apiErr.Err)
zap.L().Error(apiErr.Err.Error())
RespondError(w, apiErr, nil)
return
}
@@ -2637,13 +2636,13 @@ func (aH *APIHandler) getProducerData(w http.ResponseWriter, r *http.Request) {
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "producer", kafkaSpanEval)
if err != nil {
aH.logger.ErrorContext(r.Context(), "failed to build query range params for producer", "error", err)
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
aH.logger.ErrorContext(r.Context(), "failed to validate query range params for producer", "error", err)
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
@@ -2680,7 +2679,7 @@ func (aH *APIHandler) getConsumerData(w http.ResponseWriter, r *http.Request) {
messagingQueue, apiErr := ParseKafkaQueueBody(r)
if apiErr != nil {
aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", "error", apiErr.Err)
zap.L().Error(apiErr.Err.Error())
RespondError(w, apiErr, nil)
return
}
@@ -2690,13 +2689,13 @@ func (aH *APIHandler) getConsumerData(w http.ResponseWriter, r *http.Request) {
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "consumer", kafkaSpanEval)
if err != nil {
aH.logger.ErrorContext(r.Context(), "failed to build query range params for consumer", "error", err)
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
aH.logger.ErrorContext(r.Context(), "failed to validate query range params for consumer", "error", err)
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
@@ -2738,7 +2737,7 @@ func (aH *APIHandler) getPartitionOverviewLatencyData(w http.ResponseWriter, r *
messagingQueue, apiErr := ParseKafkaQueueBody(r)
if apiErr != nil {
aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", "error", apiErr.Err)
zap.L().Error(apiErr.Err.Error())
RespondError(w, apiErr, nil)
return
}
@@ -2748,13 +2747,13 @@ func (aH *APIHandler) getPartitionOverviewLatencyData(w http.ResponseWriter, r *
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "producer-topic-throughput", kafkaSpanEval)
if err != nil {
aH.logger.ErrorContext(r.Context(), "failed to build query range params for producer topic throughput", "error", err)
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
aH.logger.ErrorContext(r.Context(), "failed to validate query range params for producer topic throughput", "error", err)
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
@@ -2796,7 +2795,7 @@ func (aH *APIHandler) getConsumerPartitionLatencyData(w http.ResponseWriter, r *
messagingQueue, apiErr := ParseKafkaQueueBody(r)
if apiErr != nil {
aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", "error", apiErr.Err)
zap.L().Error(apiErr.Err.Error())
RespondError(w, apiErr, nil)
return
}
@@ -2806,13 +2805,13 @@ func (aH *APIHandler) getConsumerPartitionLatencyData(w http.ResponseWriter, r *
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "consumer_partition_latency", kafkaSpanEval)
if err != nil {
aH.logger.ErrorContext(r.Context(), "failed to build query range params for consumer partition latency", "error", err)
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
aH.logger.ErrorContext(r.Context(), "failed to validate query range params for consumer partition latency", "error", err)
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
@@ -2856,7 +2855,7 @@ func (aH *APIHandler) getProducerThroughputOverview(w http.ResponseWriter, r *ht
messagingQueue, apiErr := ParseKafkaQueueBody(r)
if apiErr != nil {
aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", "error", apiErr.Err)
zap.L().Error(apiErr.Err.Error())
RespondError(w, apiErr, nil)
return
}
@@ -2867,13 +2866,13 @@ func (aH *APIHandler) getProducerThroughputOverview(w http.ResponseWriter, r *ht
producerQueryRangeParams, err := kafka.BuildQRParamsWithCache(messagingQueue, "producer-throughput-overview", attributeCache)
if err != nil {
aH.logger.ErrorContext(r.Context(), "failed to build query range params for producer throughput overview", "error", err)
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
if err := validateQueryRangeParamsV3(producerQueryRangeParams); err != nil {
aH.logger.ErrorContext(r.Context(), "failed to validate query range params for producer throughput overview", "error", err)
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
@@ -2909,12 +2908,12 @@ func (aH *APIHandler) getProducerThroughputOverview(w http.ResponseWriter, r *ht
queryRangeParams, err := kafka.BuildQRParamsWithCache(messagingQueue, "producer-throughput-overview-byte-rate", attributeCache)
if err != nil {
aH.logger.ErrorContext(r.Context(), "failed to build query range params for producer throughput byte rate", "error", err)
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
aH.logger.ErrorContext(r.Context(), "failed to validate query range params for producer throughput byte rate", "error", err)
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
@@ -2972,7 +2971,7 @@ func (aH *APIHandler) getProducerThroughputDetails(w http.ResponseWriter, r *htt
messagingQueue, apiErr := ParseKafkaQueueBody(r)
if apiErr != nil {
aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", "error", apiErr.Err)
zap.L().Error(apiErr.Err.Error())
RespondError(w, apiErr, nil)
return
}
@@ -2982,13 +2981,13 @@ func (aH *APIHandler) getProducerThroughputDetails(w http.ResponseWriter, r *htt
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "producer-throughput-details", kafkaSpanEval)
if err != nil {
aH.logger.ErrorContext(r.Context(), "failed to build query range params for producer throughput details", "error", err)
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
aH.logger.ErrorContext(r.Context(), "failed to validate query range params for producer throughput details", "error", err)
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
@@ -3030,7 +3029,7 @@ func (aH *APIHandler) getConsumerThroughputOverview(w http.ResponseWriter, r *ht
messagingQueue, apiErr := ParseKafkaQueueBody(r)
if apiErr != nil {
aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", "error", apiErr.Err)
zap.L().Error(apiErr.Err.Error())
RespondError(w, apiErr, nil)
return
}
@@ -3040,13 +3039,13 @@ func (aH *APIHandler) getConsumerThroughputOverview(w http.ResponseWriter, r *ht
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "consumer-throughput-overview", kafkaSpanEval)
if err != nil {
aH.logger.ErrorContext(r.Context(), "failed to build query range params for consumer throughput overview", "error", err)
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
aH.logger.ErrorContext(r.Context(), "failed to validate query range params for consumer throughput overview", "error", err)
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
@@ -3088,7 +3087,7 @@ func (aH *APIHandler) getConsumerThroughputDetails(w http.ResponseWriter, r *htt
messagingQueue, apiErr := ParseKafkaQueueBody(r)
if apiErr != nil {
aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", "error", apiErr.Err)
zap.L().Error(apiErr.Err.Error())
RespondError(w, apiErr, nil)
return
}
@@ -3098,13 +3097,13 @@ func (aH *APIHandler) getConsumerThroughputDetails(w http.ResponseWriter, r *htt
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "consumer-throughput-details", kafkaSpanEval)
if err != nil {
aH.logger.ErrorContext(r.Context(), "failed to build query range params for consumer throughput details", "error", err)
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
aH.logger.ErrorContext(r.Context(), "failed to validate query range params for consumer throughput details", "error", err)
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
@@ -3149,7 +3148,7 @@ func (aH *APIHandler) getProducerConsumerEval(w http.ResponseWriter, r *http.Req
messagingQueue, apiErr := ParseKafkaQueueBody(r)
if apiErr != nil {
aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", "error", apiErr.Err)
zap.L().Error(apiErr.Err.Error())
RespondError(w, apiErr, nil)
return
}
@@ -3159,7 +3158,7 @@ func (aH *APIHandler) getProducerConsumerEval(w http.ResponseWriter, r *http.Req
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "producer-consumer-eval", kafkaSpanEval)
if err != nil {
aH.logger.ErrorContext(r.Context(), "failed to build query range params for producer consumer eval", "error", err)
zap.L().Error(err.Error())
RespondError(w, &model.ApiError{
Typ: model.ErrorBadData,
Err: err,
@@ -3168,7 +3167,7 @@ func (aH *APIHandler) getProducerConsumerEval(w http.ResponseWriter, r *http.Req
}
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
aH.logger.ErrorContext(r.Context(), "failed to validate query range params for producer consumer eval", "error", err)
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
@@ -4256,7 +4255,7 @@ func (aH *APIHandler) CreateLogsPipeline(w http.ResponseWriter, r *http.Request)
postable []pipelinetypes.PostablePipeline,
) (*logparsingpipeline.PipelinesResponse, error) {
if len(postable) == 0 {
aH.logger.WarnContext(r.Context(), "found no pipelines in the http request, this will delete all the pipelines")
zap.L().Warn("found no pipelines in the http request, this will delete all the pipelines")
}
err := aH.LogsParsingPipelineController.ValidatePipelines(ctx, postable)
@@ -4454,7 +4453,7 @@ func (aH *APIHandler) QueryRangeV3Format(w http.ResponseWriter, r *http.Request)
queryRangeParams, apiErrorObj := ParseQueryRangeParams(r)
if apiErrorObj != nil {
aH.logger.ErrorContext(r.Context(), "error parsing query range params", "error", apiErrorObj.Err)
zap.L().Error(apiErrorObj.Err.Error())
RespondError(w, apiErrorObj, nil)
return
}
@@ -4516,13 +4515,13 @@ func (aH *APIHandler) queryRangeV3(ctx context.Context, queryRangeParams *v3.Que
// check if traceID is used as filter (with equal/similar operator) in traces query if yes add timestamp filter to queryRange params
isUsed, traceIDs := tracesV3.TraceIdFilterUsedWithEqual(queryRangeParams)
if isUsed && len(traceIDs) > 0 {
aH.logger.DebugContext(ctx, "trace_id used as filter in traces query")
zap.L().Debug("traceID used as filter in traces query")
// query signoz_spans table with traceID to get min and max timestamp
min, max, err := aH.reader.GetMinAndMaxTimestampForTraceID(ctx, traceIDs)
if err == nil {
// add timestamp filter to queryRange params
tracesV3.AddTimestampFilters(min, max, queryRangeParams)
aH.logger.DebugContext(ctx, "post adding timestamp filter in traces query", "query_range_params", queryRangeParams)
zap.L().Debug("post adding timestamp filter in traces query", zap.Any("queryRangeParams", queryRangeParams))
}
}
}
@@ -4533,8 +4532,9 @@ func (aH *APIHandler) queryRangeV3(ctx context.Context, queryRangeParams *v3.Que
onQueryFinished, apiErr := aH.reader.ReportQueryStartForProgressTracking(queryIdHeader)
if apiErr != nil {
aH.logger.ErrorContext(ctx, "failed to report query start for progress tracking",
"query_id", queryIdHeader, "error", apiErr,
zap.L().Error(
"couldn't report query start for progress tracking",
zap.String("queryId", queryIdHeader), zap.Error(apiErr),
)
} else {
@@ -4709,7 +4709,7 @@ func (aH *APIHandler) QueryRangeV3(w http.ResponseWriter, r *http.Request) {
queryRangeParams, apiErrorObj := ParseQueryRangeParams(r)
if apiErrorObj != nil {
aH.logger.ErrorContext(r.Context(), "error parsing metric query range params", "error", apiErrorObj.Err)
zap.L().Error("error parsing metric query range params", zap.Error(apiErrorObj.Err))
RespondError(w, apiErrorObj, nil)
return
}
@@ -4717,7 +4717,7 @@ func (aH *APIHandler) QueryRangeV3(w http.ResponseWriter, r *http.Request) {
// add temporality for each metric
temporalityErr := aH.PopulateTemporality(r.Context(), orgID, queryRangeParams)
if temporalityErr != nil {
aH.logger.ErrorContext(r.Context(), "error adding temporality for metrics", "error", temporalityErr)
zap.L().Error("Error while adding temporality for metrics", zap.Error(temporalityErr))
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: temporalityErr}, nil)
return
}
@@ -4761,8 +4761,9 @@ func (aH *APIHandler) GetQueryProgressUpdates(w http.ResponseWriter, r *http.Req
progressCh, unsubscribe, apiErr := aH.reader.SubscribeToQueryProgress(queryId)
if apiErr != nil {
// Shouldn't happen unless query progress requested after query finished
aH.logger.WarnContext(r.Context(), "failed to subscribe to query progress",
"query_id", queryId, "error", apiErr,
zap.L().Warn(
"couldn't subscribe to query progress",
zap.String("queryId", queryId), zap.Any("error", apiErr),
)
return
}
@@ -4771,22 +4772,25 @@ func (aH *APIHandler) GetQueryProgressUpdates(w http.ResponseWriter, r *http.Req
for queryProgress := range progressCh {
msg, err := json.Marshal(queryProgress)
if err != nil {
aH.logger.ErrorContext(r.Context(), "failed to serialize progress message",
"query_id", queryId, "progress", queryProgress, "error", err,
zap.L().Error(
"failed to serialize progress message",
zap.String("queryId", queryId), zap.Any("progress", queryProgress), zap.Error(err),
)
continue
}
err = c.WriteMessage(websocket.TextMessage, msg)
if err != nil {
aH.logger.ErrorContext(r.Context(), "failed to write progress message to websocket",
"query_id", queryId, "msg", string(msg), "error", err,
zap.L().Error(
"failed to write progress msg to websocket",
zap.String("queryId", queryId), zap.String("msg", string(msg)), zap.Error(err),
)
break
} else {
aH.logger.DebugContext(r.Context(), "wrote progress message to websocket",
"query_id", queryId, "msg", string(msg),
zap.L().Debug(
"wrote progress msg to websocket",
zap.String("queryId", queryId), zap.String("msg", string(msg)), zap.Error(err),
)
}
}
@@ -4870,13 +4874,13 @@ func (aH *APIHandler) queryRangeV4(ctx context.Context, queryRangeParams *v3.Que
// check if traceID is used as filter (with equal/similar operator) in traces query if yes add timestamp filter to queryRange params
isUsed, traceIDs := tracesV3.TraceIdFilterUsedWithEqual(queryRangeParams)
if isUsed && len(traceIDs) > 0 {
aH.logger.DebugContext(ctx, "trace_id used as filter in traces query")
zap.L().Debug("traceID used as filter in traces query")
// query signoz_spans table with traceID to get min and max timestamp
min, max, err := aH.reader.GetMinAndMaxTimestampForTraceID(ctx, traceIDs)
if err == nil {
// add timestamp filter to queryRange params
tracesV3.AddTimestampFilters(min, max, queryRangeParams)
aH.logger.DebugContext(ctx, "post adding timestamp filter in traces query", "query_range_params", queryRangeParams)
zap.L().Debug("post adding timestamp filter in traces query", zap.Any("queryRangeParams", queryRangeParams))
}
}
}
@@ -4928,7 +4932,7 @@ func (aH *APIHandler) QueryRangeV4(w http.ResponseWriter, r *http.Request) {
queryRangeParams, apiErrorObj := ParseQueryRangeParams(r)
if apiErrorObj != nil {
aH.logger.ErrorContext(r.Context(), "error parsing metric query range params", "error", apiErrorObj.Err)
zap.L().Error("error parsing metric query range params", zap.Error(apiErrorObj.Err))
RespondError(w, apiErrorObj, nil)
return
}
@@ -4937,7 +4941,7 @@ func (aH *APIHandler) QueryRangeV4(w http.ResponseWriter, r *http.Request) {
// add temporality for each metric
temporalityErr := aH.PopulateTemporality(r.Context(), orgID, queryRangeParams)
if temporalityErr != nil {
aH.logger.ErrorContext(r.Context(), "error adding temporality for metrics", "error", temporalityErr)
zap.L().Error("Error while adding temporality for metrics", zap.Error(temporalityErr))
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: temporalityErr}, nil)
return
}
@@ -4986,7 +4990,7 @@ func (aH *APIHandler) getQueueOverview(w http.ResponseWriter, r *http.Request) {
queueListRequest, apiErr := ParseQueueBody(r)
if apiErr != nil {
aH.logger.ErrorContext(r.Context(), "failed to parse queue body", "error", apiErr.Err)
zap.L().Error(apiErr.Err.Error())
RespondError(w, apiErr, nil)
return
}
@@ -4994,7 +4998,7 @@ func (aH *APIHandler) getQueueOverview(w http.ResponseWriter, r *http.Request) {
chq, err := queues2.BuildOverviewQuery(queueListRequest)
if err != nil {
aH.logger.ErrorContext(r.Context(), "failed to build queue overview query", "error", err)
zap.L().Error(err.Error())
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error building clickhouse query: %v", err)}, nil)
return
}
@@ -5025,7 +5029,7 @@ func (aH *APIHandler) getDomainList(w http.ResponseWriter, r *http.Request) {
// Parse the request body to get third-party query parameters
thirdPartyQueryRequest, apiErr := ParseRequestBody(r)
if apiErr != nil {
aH.logger.ErrorContext(r.Context(), "failed to parse request body", "error", apiErr)
zap.L().Error("Failed to parse request body", zap.Error(apiErr))
render.Error(w, errorsV2.New(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, apiErr.Error()))
return
}
@@ -5033,7 +5037,7 @@ func (aH *APIHandler) getDomainList(w http.ResponseWriter, r *http.Request) {
// Build the v5 query range request for domain listing
queryRangeRequest, err := thirdpartyapi.BuildDomainList(thirdPartyQueryRequest)
if err != nil {
aH.logger.ErrorContext(r.Context(), "failed to build domain list query", "error", err)
zap.L().Error("Failed to build domain list query", zap.Error(err))
apiErrObj := errorsV2.New(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, err.Error())
render.Error(w, apiErrObj)
return
@@ -5046,7 +5050,7 @@ func (aH *APIHandler) getDomainList(w http.ResponseWriter, r *http.Request) {
// Execute the query using the v5 querier
result, err := aH.Signoz.Querier.QueryRange(ctx, orgID, queryRangeRequest)
if err != nil {
aH.logger.ErrorContext(r.Context(), "query execution failed", "error", err)
zap.L().Error("Query execution failed", zap.Error(err))
apiErrObj := errorsV2.New(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, err.Error())
render.Error(w, apiErrObj)
return
@@ -5085,7 +5089,7 @@ func (aH *APIHandler) getDomainInfo(w http.ResponseWriter, r *http.Request) {
// Parse the request body to get third-party query parameters
thirdPartyQueryRequest, apiErr := ParseRequestBody(r)
if apiErr != nil {
aH.logger.ErrorContext(r.Context(), "failed to parse request body", "error", apiErr)
zap.L().Error("Failed to parse request body", zap.Error(apiErr))
render.Error(w, errorsV2.New(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, apiErr.Error()))
return
}
@@ -5093,7 +5097,7 @@ func (aH *APIHandler) getDomainInfo(w http.ResponseWriter, r *http.Request) {
// Build the v5 query range request for domain info
queryRangeRequest, err := thirdpartyapi.BuildDomainInfo(thirdPartyQueryRequest)
if err != nil {
aH.logger.ErrorContext(r.Context(), "failed to build domain info query", "error", err)
zap.L().Error("Failed to build domain info query", zap.Error(err))
apiErrObj := errorsV2.New(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, err.Error())
render.Error(w, apiErrObj)
return
@@ -5106,7 +5110,7 @@ func (aH *APIHandler) getDomainInfo(w http.ResponseWriter, r *http.Request) {
// Execute the query using the v5 querier
result, err := aH.Signoz.Querier.QueryRange(ctx, orgID, queryRangeRequest)
if err != nil {
aH.logger.ErrorContext(r.Context(), "query execution failed", "error", err)
zap.L().Error("Query execution failed", zap.Error(err))
apiErrObj := errorsV2.New(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, err.Error())
render.Error(w, apiErrObj)
return

View File

@@ -17,10 +17,9 @@ import (
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/query-service/postprocess"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"log/slog"
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"go.uber.org/zap"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
)
@@ -428,7 +427,7 @@ func (h *HostsRepo) GetHostList(ctx context.Context, orgID valuer.UUID, req mode
step := int64(math.Max(float64(common.MinAllowedStepInterval(req.Start, req.End)), 60))
if step <= 0 {
slog.ErrorContext(ctx, "step is less than or equal to 0", "step", step)
zap.L().Error("step is less than or equal to 0", zap.Int64("step", step))
return resp, errors.New("step is less than or equal to 0")
}

View File

@@ -8,11 +8,10 @@ import (
"gopkg.in/yaml.v3"
"log/slog"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/query-service/constants"
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
"go.uber.org/zap"
)
var lockLogsPipelineSpec sync.RWMutex
@@ -155,14 +154,14 @@ func buildCollectorPipelineProcessorsList(
func checkDuplicateString(pipeline []string) bool {
exists := make(map[string]bool, len(pipeline))
slog.Debug("checking duplicate processors in the pipeline", "pipeline", pipeline)
zap.L().Debug("checking duplicate processors in the pipeline:", zap.Any("pipeline", pipeline))
for _, processor := range pipeline {
name := processor
if _, ok := exists[name]; ok {
slog.Error(
zap.L().Error(
"duplicate processor name detected in generated collector config for log pipelines",
"processor", processor,
"pipeline", pipeline,
zap.String("processor", processor),
zap.Any("pipeline", pipeline),
)
return true
}

View File

@@ -21,7 +21,7 @@ import (
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/google/uuid"
"log/slog"
"go.uber.org/zap"
)
var (
@@ -175,7 +175,7 @@ func (ic *LogParsingPipelineController) getEffectivePipelinesByVersion(
if version >= 0 {
savedPipelines, err := ic.getPipelinesByVersion(ctx, orgID.String(), version)
if err != nil {
slog.ErrorContext(ctx, "failed to get pipelines for version", "version", version, "error", err)
zap.L().Error("failed to get pipelines for version", zap.Int("version", version), zap.Error(err))
return nil, err
}
result = savedPipelines
@@ -227,7 +227,7 @@ func (ic *LogParsingPipelineController) GetPipelinesByVersion(
) (*PipelinesResponse, error) {
pipelines, err := ic.getEffectivePipelinesByVersion(ctx, orgId, version)
if err != nil {
slog.ErrorContext(ctx, "failed to get pipelines for version", "version", version, "error", err)
zap.L().Error("failed to get pipelines for version", zap.Int("version", version), zap.Error(err))
return nil, err
}
@@ -235,7 +235,7 @@ func (ic *LogParsingPipelineController) GetPipelinesByVersion(
if version >= 0 {
cv, err := agentConf.GetConfigVersion(ctx, orgId, opamptypes.ElementTypeLogPipelines, version)
if err != nil {
slog.ErrorContext(ctx, "failed to get config for version", "version", version, "error", err)
zap.L().Error("failed to get config for version", zap.Int("version", version), zap.Error(err))
return nil, err
}
configVersion = cv

View File

@@ -6,8 +6,6 @@ import (
"fmt"
"time"
"log/slog"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/sqlstore"
@@ -15,6 +13,7 @@ import (
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
"github.com/SigNoz/signoz/pkg/valuer"
"go.uber.org/zap"
)
// Repo handles DDL and DML ops on ingestion pipeline
@@ -81,7 +80,7 @@ func (r *Repo) insertPipeline(
Model(&insertRow.StoreablePipeline).
Exec(ctx)
if err != nil {
slog.ErrorContext(ctx, "error in inserting pipeline data", "error", err)
zap.L().Error("error in inserting pipeline data", zap.Error(err))
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to insert pipeline")
}
@@ -137,12 +136,12 @@ func (r *Repo) GetPipeline(
Where("org_id = ?", orgID).
Scan(ctx)
if err != nil {
slog.ErrorContext(ctx, "failed to get ingestion pipeline from db", "error", err)
zap.L().Error("failed to get ingestion pipeline from db", zap.Error(err))
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to get ingestion pipeline from db")
}
if len(storablePipelines) == 0 {
slog.WarnContext(ctx, "no row found for ingestion pipeline id", "id", id)
zap.L().Warn("No row found for ingestion pipeline id", zap.String("id", id))
return nil, errors.NewNotFoundf(errors.CodeNotFound, "no row found for ingestion pipeline id %v", id)
}
@@ -150,11 +149,11 @@ func (r *Repo) GetPipeline(
gettablePipeline := pipelinetypes.GettablePipeline{}
gettablePipeline.StoreablePipeline = storablePipelines[0]
if err := gettablePipeline.ParseRawConfig(); err != nil {
slog.ErrorContext(ctx, "invalid pipeline config found", "id", id, "error", err)
zap.L().Error("invalid pipeline config found", zap.String("id", id), zap.Error(err))
return nil, err
}
if err := gettablePipeline.ParseFilter(); err != nil {
slog.ErrorContext(ctx, "invalid pipeline filter found", "id", id, "error", err)
zap.L().Error("invalid pipeline filter found", zap.String("id", id), zap.Error(err))
return nil, err
}
return &gettablePipeline, nil

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