From 7df1f25bcd0d4e4ab7d2b4eb06db531f5fb84f2c Mon Sep 17 00:00:00 2001 From: Veer Shah Date: Fri, 20 Mar 2026 17:25:24 +0530 Subject: [PATCH 01/78] fix(ui): add cursor pointer on External APIs domain rows (#10654) The domain rows in the External APIs table are clickable (they open a detail drawer) but showed the default cursor, giving no visual affordance. Added cursor: pointer to the .expanded-clickable-row class that is already applied via onRow() in DomainList.tsx. Fixes #9403 --- .../src/container/ApiMonitoring/Explorer/Explorer.styles.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/src/container/ApiMonitoring/Explorer/Explorer.styles.scss b/frontend/src/container/ApiMonitoring/Explorer/Explorer.styles.scss index 265522f84db..7fcc7016212 100644 --- a/frontend/src/container/ApiMonitoring/Explorer/Explorer.styles.scss +++ b/frontend/src/container/ApiMonitoring/Explorer/Explorer.styles.scss @@ -144,6 +144,10 @@ background: var(--bg-ink-300); } + .expanded-clickable-row { + cursor: pointer; + } + .error-rate { width: 120px; } From 78b08369746cdf2d0ac939ffe2b05b38e15fbc67 Mon Sep 17 00:00:00 2001 From: Debopam Roy <108235552+debopam-roy@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:07:10 +0530 Subject: [PATCH 02/78] fix(api-monitoring): border being hidden on hover (#9415) * feat: change the cursor to pointer on hovering domain rows * feat: external api -> domains table -> right drawer -> all the columns have same color * feat: hover style of the left tab when the right tab is active --- .../Domains/DomainDetails/DomainDetails.styles.scss | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/DomainDetails.styles.scss b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/DomainDetails.styles.scss index 5c5bec89822..f2250f6211d 100644 --- a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/DomainDetails.styles.scss +++ b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/DomainDetails.styles.scss @@ -700,11 +700,17 @@ .ant-btn { box-shadow: none; + position: relative; } .tab { border: 1px solid var(--bg-slate-400); width: 114px; + z-index: 1; + } + + .tab:hover { + z-index: 3; } .tab::before { @@ -715,6 +721,7 @@ background: var(--bg-slate-400); color: var(--text-vanilla-100); border: 1px solid var(--bg-slate-400); + z-index: 2; } .selected_view::before { From 1a85ccb37311ad9b6bcd8fa7d0e3fd8ab7cdb6bb Mon Sep 17 00:00:00 2001 From: Pandey Date: Fri, 20 Mar 2026 21:32:21 +0530 Subject: [PATCH 03/78] chore: remove unused config files from conf/ (#10663) Remove `conf/prometheus.yml` and `conf/cache-config.yml` which are legacy artifacts with zero references anywhere in the codebase. --- conf/cache-config.yml | 4 ---- conf/prometheus.yml | 25 ------------------------- 2 files changed, 29 deletions(-) delete mode 100644 conf/cache-config.yml delete mode 100644 conf/prometheus.yml diff --git a/conf/cache-config.yml b/conf/cache-config.yml deleted file mode 100644 index b1bd329584c..00000000000 --- a/conf/cache-config.yml +++ /dev/null @@ -1,4 +0,0 @@ -provider: "inmemory" -inmemory: - ttl: 60m - cleanupInterval: 10m diff --git a/conf/prometheus.yml b/conf/prometheus.yml deleted file mode 100644 index 6513cd0c3f8..00000000000 --- a/conf/prometheus.yml +++ /dev/null @@ -1,25 +0,0 @@ -# my global config -global: - scrape_interval: 5s # Set the scrape interval to every 15 seconds. Default is every 1 minute. - evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. - # scrape_timeout is set to the global default (10s). - -# Alertmanager configuration -alerting: - alertmanagers: - - static_configs: - - targets: - - 127.0.0.1:9093 - -# Load rules once and periodically evaluate them according to the global 'evaluation_interval'. -rule_files: - # - "first_rules.yml" - # - "second_rules.yml" - - 'alerts.yml' - -# A scrape configuration containing exactly one endpoint to scrape: -# Here it's Prometheus itself. -scrape_configs: [] - -remote_read: - - url: tcp://localhost:9000/signoz_metrics From 95ed125bd96f056ee38100fd5a56605256c3b072 Mon Sep 17 00:00:00 2001 From: Pandey Date: Sun, 22 Mar 2026 09:36:31 +0530 Subject: [PATCH 04/78] feat(instrumentation): add OTel exception semantic convention log handler (#10665) * feat(instrumentation): add OTel exception semantic convention log handler Add a loghandler.Wrapper that enriches error log records with OpenTelemetry exception semantic convention attributes (exception.type, exception.code, exception.message, exception.stacktrace). - Add errors.Attr() helper for standardized error logging under "exception" key - Add exception log handler that replaces raw error attrs with structured group - Wire exception handler into the instrumentation SDK logger chain - Remove LogValue() from errors.base as the handler now owns structuring * refactor: replace "error", err with errors.Attr(err) across codebase Migrate all slog error logging from ad-hoc "error", err key-value pairs to the standardized errors.Attr(err) helper, enabling the exception log handler to enrich these logs with OTel semantic convention attributes. * refactor: enforce attr-only slog style across codebase Change sloglint from kv-only to attr-only, requiring all slog calls to use typed attributes (slog.String, slog.Any, etc.) instead of key-value pairs. Convert all existing kv-style slog calls in non-excluded paths. * refactor: tighten slog.Any to specific types and standardize error attrs - Replace slog.Any with slog.String for string values (action, key, where_clause) - Replace slog.Any with slog.Uint64 for uint64 values (start, end, step, etc.) - Replace slog.Any("err", err) with errors.Attr(err) in dispatcher and segment analytics - Replace slog.Any("error", ctx.Err()) with errors.Attr in factory registry * fix(instrumentation): use Unwrapb message for exception.message Use the explicit error message (m) from Unwrapb instead of foundErr.Error(), which resolves to the inner cause's message for wrapped errors. * feat(errors): capture stacktrace at error creation time Store program counters ([]uintptr) in base errors at creation time using runtime.Callers, inspired by thanos-io/thanos/pkg/errors. The exception log handler reads the stacktrace from the error instead of capturing at log time, showing where the error originated. * fix(instrumentation): apply default log wrappers uniformly in NewLogger Move correlation, filtering, and exception wrappers into NewLogger so all call sites (including CLI loggers in cmd/) get them automatically. * refactor(instrumentation): remove variadic wrappers from NewLogger NewLogger no longer accepts arbitrary wrappers. The core wrappers (correlation, filtering, exception) are hardcoded, preventing callers from accidentally duplicating behavior. * refactor: migrate remaining "error", to errors.Attr across legacy paths Replace all remaining "error", key-value pairs with errors.Attr() in pkg/query-service/ and ee/query-service/ paths that were missed in the initial migration due to non-standard variable names (res.Err, filterErr, apiErrorObj.Err, etc). * refactor(instrumentation): use flat exception.* keys instead of nested group Use flat keys (exception.type, exception.code, exception.message, exception.stacktrace) instead of a nested slog.Group in the exception log handler. --- .golangci.yml | 2 +- cmd/community/server.go | 16 +- cmd/enterprise/server.go | 20 +- cmd/root.go | 6 +- ee/anomaly/seasonal.go | 40 +-- .../callbackauthn/oidccallbackauthn/authn.go | 3 +- ee/licensing/httplicensing/provider.go | 10 +- ee/querier/handler.go | 9 +- ee/query-service/app/api/featureFlags.go | 10 +- ee/query-service/app/api/queryrange.go | 5 +- ee/query-service/app/server.go | 20 +- ee/query-service/rules/anomaly.go | 7 +- ee/query-service/rules/manager.go | 14 +- ee/query-service/usage/manager.go | 15 +- ee/sqlschema/postgressqlschema/provider.go | 9 +- .../alertmanagerbatcher/batcher.go | 4 +- .../alertmanagernotify/msteamsv2/msteamsv2.go | 2 +- .../alertmanagernotify/receiver.go | 2 +- .../alertmanagerserver/dispatcher.go | 24 +- pkg/alertmanager/alertmanagerserver/server.go | 17 +- .../rulebasednotification/provider.go | 3 +- pkg/alertmanager/service.go | 13 +- .../signozalertmanager/provider.go | 7 +- pkg/analytics/segmentanalytics/provider.go | 26 +- .../googlecallbackauthn/authn.go | 38 +-- pkg/cache/rediscache/provider.go | 9 +- pkg/emailing/noopemailing/provider.go | 3 +- pkg/emailing/smtpemailing/provider.go | 5 +- .../templatestore/filetemplatestore/store.go | 4 +- pkg/errors/errors.go | 31 ++- pkg/errors/errors_test.go | 7 + pkg/errors/stacktrace.go | 38 +++ pkg/factory/registry.go | 12 +- pkg/factory/settings.go | 2 +- pkg/flagger/configflagger/configflagger.go | 11 +- pkg/flagger/flagger.go | 47 ++-- pkg/http/client/plugin/log.go | 6 +- pkg/http/middleware/authz.go | 14 +- pkg/http/middleware/identn.go | 3 +- pkg/http/middleware/logging.go | 6 +- pkg/http/middleware/timeout.go | 6 +- pkg/http/server/server.go | 8 +- pkg/identn/apikeyidentn/provider.go | 5 +- pkg/identn/resolver.go | 3 +- pkg/identn/tokenizeridentn/provider.go | 6 +- pkg/instrumentation/logger.go | 6 +- pkg/instrumentation/loghandler/exception.go | 53 ++++ .../loghandler/exception_test.go | 100 +++++++ .../loghandler/filtering_test.go | 6 +- pkg/instrumentation/sdk.go | 3 +- pkg/modules/dashboard/impldashboard/module.go | 5 +- .../implmetricsexplorer/module.go | 13 +- .../implserviceaccount/module.go | 4 +- pkg/modules/session/implsession/module.go | 3 +- pkg/modules/user/impluser/module.go | 14 +- pkg/modules/user/impluser/service.go | 2 +- pkg/querier/api.go | 17 +- pkg/querier/bucket_cache.go | 34 +-- pkg/querier/postprocess.go | 11 +- pkg/querier/promql_query.go | 11 +- pkg/querier/querier.go | 19 +- pkg/query-service/agentConf/db.go | 15 +- pkg/query-service/agentConf/manager.go | 24 +- .../clickhouseReader/filter_suggestions.go | 5 +- .../app/clickhouseReader/reader.go | 245 +++++++++--------- pkg/query-service/app/http_handler.go | 153 +++++------ .../app/logparsingpipeline/controller.go | 9 +- .../app/logparsingpipeline/db.go | 8 +- .../app/metricsexplorer/summary.go | 14 +- .../app/opamp/configure_ingestionRules.go | 13 +- pkg/query-service/app/opamp/model/agent.go | 8 +- pkg/query-service/app/opamp/opamp_server.go | 12 +- pkg/query-service/app/parser.go | 9 +- pkg/query-service/app/querier/v2/helper.go | 10 +- pkg/query-service/app/server.go | 20 +- pkg/query-service/app/summary.go | 31 +-- pkg/query-service/app/traces/smart/trace.go | 3 +- pkg/query-service/app/traces/v3/utils.go | 3 +- pkg/query-service/model/v3/v3.go | 6 +- pkg/query-service/postprocess/process.go | 6 +- .../querycache/query_range_cache.go | 5 +- pkg/query-service/rules/base_rule.go | 4 +- pkg/query-service/rules/manager.go | 32 +-- pkg/query-service/rules/prom_rule.go | 11 +- pkg/query-service/rules/prom_rule_task.go | 10 +- pkg/query-service/rules/rule_task.go | 8 +- pkg/query-service/rules/test_notification.go | 13 +- pkg/query-service/rules/threshold_rule.go | 13 +- pkg/querybuilder/where_clause_visitor.go | 2 +- pkg/queryparser/api.go | 3 +- pkg/ruler/rulestore/sqlrulestore/rule.go | 6 +- pkg/signoz/signoz.go | 5 +- pkg/smtp/client/smtp.go | 2 +- ...pdate_dashboard_alert_and_saved_view_v5.go | 16 +- ...migrate_rules_v4_to_v5_post_deprecation.go | 26 +- pkg/sqlmigrator/migrator.go | 22 +- pkg/sqlschema/sqlitesqlschema/provider.go | 6 +- pkg/sqlstore/bun.go | 7 +- pkg/sqlstore/sqlitesqlstore/provider.go | 3 +- pkg/sqlstore/sqlstorehook/logging.go | 8 +- .../analyticsstatsreporter/provider.go | 25 +- pkg/telemetrylogs/statement_builder.go | 2 +- pkg/telemetrymetadata/body_json_metadata.go | 5 +- pkg/telemetrymetadata/metadata.go | 17 +- pkg/telemetrytraces/statement_builder.go | 6 +- .../trace_operator_cte_builder.go | 48 ++-- .../trace_operator_statement_builder.go | 4 +- pkg/tokenizer/jwttokenizer/provider.go | 20 +- pkg/tokenizer/opaquetokenizer/provider.go | 20 +- pkg/transition/migrate_alert.go | 6 +- pkg/transition/migrate_common.go | 20 +- pkg/transition/migrate_dashboard.go | 8 +- pkg/transition/migrate_saved_view.go | 2 +- 113 files changed, 1083 insertions(+), 745 deletions(-) create mode 100644 pkg/errors/stacktrace.go create mode 100644 pkg/instrumentation/loghandler/exception.go create mode 100644 pkg/instrumentation/loghandler/exception_test.go diff --git a/.golangci.yml b/.golangci.yml index 5d801f24b47..b076724dd73 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -35,7 +35,7 @@ linters: - identical sloglint: no-mixed-args: true - kv-only: true + attr-only: true no-global: all context: all static-msg: true diff --git a/cmd/community/server.go b/cmd/community/server.go index b0b7425ee25..a58eb1682e9 100644 --- a/cmd/community/server.go +++ b/cmd/community/server.go @@ -4,12 +4,15 @@ import ( "context" "log/slog" + "github.com/spf13/cobra" + "github.com/SigNoz/signoz/cmd" "github.com/SigNoz/signoz/pkg/analytics" "github.com/SigNoz/signoz/pkg/authn" "github.com/SigNoz/signoz/pkg/authz" "github.com/SigNoz/signoz/pkg/authz/openfgaauthz" "github.com/SigNoz/signoz/pkg/authz/openfgaschema" + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/gateway" "github.com/SigNoz/signoz/pkg/gateway/noopgateway" @@ -28,7 +31,6 @@ import ( "github.com/SigNoz/signoz/pkg/version" "github.com/SigNoz/signoz/pkg/zeus" "github.com/SigNoz/signoz/pkg/zeus/noopzeus" - "github.com/spf13/cobra" ) func registerServer(parentCmd *cobra.Command, logger *slog.Logger) { @@ -90,37 +92,37 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e }, ) if err != nil { - logger.ErrorContext(ctx, "failed to create signoz", "error", err) + logger.ErrorContext(ctx, "failed to create signoz", errors.Attr(err)) return err } server, err := app.NewServer(config, signoz) if err != nil { - logger.ErrorContext(ctx, "failed to create server", "error", err) + logger.ErrorContext(ctx, "failed to create server", errors.Attr(err)) return err } if err := server.Start(ctx); err != nil { - logger.ErrorContext(ctx, "failed to start server", "error", err) + logger.ErrorContext(ctx, "failed to start server", errors.Attr(err)) return err } signoz.Start(ctx) if err := signoz.Wait(ctx); err != nil { - logger.ErrorContext(ctx, "failed to start signoz", "error", err) + logger.ErrorContext(ctx, "failed to start signoz", errors.Attr(err)) return err } err = server.Stop(ctx) if err != nil { - logger.ErrorContext(ctx, "failed to stop server", "error", err) + logger.ErrorContext(ctx, "failed to stop server", errors.Attr(err)) return err } err = signoz.Stop(ctx) if err != nil { - logger.ErrorContext(ctx, "failed to stop signoz", "error", err) + logger.ErrorContext(ctx, "failed to stop signoz", errors.Attr(err)) return err } diff --git a/cmd/enterprise/server.go b/cmd/enterprise/server.go index d0e28da307a..709b4a75aff 100644 --- a/cmd/enterprise/server.go +++ b/cmd/enterprise/server.go @@ -5,16 +5,18 @@ import ( "log/slog" "time" + "github.com/spf13/cobra" + "github.com/SigNoz/signoz/cmd" "github.com/SigNoz/signoz/ee/authn/callbackauthn/oidccallbackauthn" "github.com/SigNoz/signoz/ee/authn/callbackauthn/samlcallbackauthn" "github.com/SigNoz/signoz/ee/authz/openfgaauthz" - eequerier "github.com/SigNoz/signoz/ee/querier" "github.com/SigNoz/signoz/ee/authz/openfgaschema" "github.com/SigNoz/signoz/ee/gateway/httpgateway" enterpriselicensing "github.com/SigNoz/signoz/ee/licensing" "github.com/SigNoz/signoz/ee/licensing/httplicensing" "github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard" + eequerier "github.com/SigNoz/signoz/ee/querier" enterpriseapp "github.com/SigNoz/signoz/ee/query-service/app" "github.com/SigNoz/signoz/ee/sqlschema/postgressqlschema" "github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore" @@ -23,6 +25,7 @@ import ( "github.com/SigNoz/signoz/pkg/analytics" "github.com/SigNoz/signoz/pkg/authn" "github.com/SigNoz/signoz/pkg/authz" + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/gateway" "github.com/SigNoz/signoz/pkg/licensing" @@ -38,7 +41,6 @@ import ( "github.com/SigNoz/signoz/pkg/types/authtypes" "github.com/SigNoz/signoz/pkg/version" "github.com/SigNoz/signoz/pkg/zeus" - "github.com/spf13/cobra" ) func registerServer(parentCmd *cobra.Command, logger *slog.Logger) { @@ -69,7 +71,7 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e // add enterprise sqlstore factories to the community sqlstore factories sqlstoreFactories := signoz.NewSQLStoreProviderFactories() if err := sqlstoreFactories.Add(postgressqlstore.NewFactory(sqlstorehook.NewLoggingFactory(), sqlstorehook.NewInstrumentationFactory())); err != nil { - logger.ErrorContext(ctx, "failed to add postgressqlstore factory", "error", err) + logger.ErrorContext(ctx, "failed to add postgressqlstore factory", errors.Attr(err)) return err } @@ -132,37 +134,37 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e ) if err != nil { - logger.ErrorContext(ctx, "failed to create signoz", "error", err) + logger.ErrorContext(ctx, "failed to create signoz", errors.Attr(err)) return err } server, err := enterpriseapp.NewServer(config, signoz) if err != nil { - logger.ErrorContext(ctx, "failed to create server", "error", err) + logger.ErrorContext(ctx, "failed to create server", errors.Attr(err)) return err } if err := server.Start(ctx); err != nil { - logger.ErrorContext(ctx, "failed to start server", "error", err) + logger.ErrorContext(ctx, "failed to start server", errors.Attr(err)) return err } signoz.Start(ctx) if err := signoz.Wait(ctx); err != nil { - logger.ErrorContext(ctx, "failed to start signoz", "error", err) + logger.ErrorContext(ctx, "failed to start signoz", errors.Attr(err)) return err } err = server.Stop(ctx) if err != nil { - logger.ErrorContext(ctx, "failed to stop server", "error", err) + logger.ErrorContext(ctx, "failed to stop server", errors.Attr(err)) return err } err = signoz.Stop(ctx) if err != nil { - logger.ErrorContext(ctx, "failed to stop signoz", "error", err) + logger.ErrorContext(ctx, "failed to stop signoz", errors.Attr(err)) return err } diff --git a/cmd/root.go b/cmd/root.go index 9e7aee7b04f..e601aa513eb 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,8 +4,10 @@ import ( "log/slog" "os" - "github.com/SigNoz/signoz/pkg/version" "github.com/spf13/cobra" + + "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/version" ) var RootCmd = &cobra.Command{ @@ -20,7 +22,7 @@ var RootCmd = &cobra.Command{ func Execute(logger *slog.Logger) { err := RootCmd.Execute() if err != nil { - logger.ErrorContext(RootCmd.Context(), "error running command", "error", err) + logger.ErrorContext(RootCmd.Context(), "error running command", errors.Attr(err)) os.Exit(1) } } diff --git a/ee/anomaly/seasonal.go b/ee/anomaly/seasonal.go index ffee2c1d1d4..8753289ad81 100644 --- a/ee/anomaly/seasonal.go +++ b/ee/anomaly/seasonal.go @@ -74,37 +74,37 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, orgID valuer.UUID instrumentationtypes.CodeFunctionName: "getResults", }) // TODO(srikanthccv): parallelize this? - p.logger.InfoContext(ctx, "fetching results for current period", "anomaly_current_period_query", params.CurrentPeriodQuery) + p.logger.InfoContext(ctx, "fetching results for current period", slog.Any("anomaly_current_period_query", params.CurrentPeriodQuery)) currentPeriodResults, err := p.querier.QueryRange(ctx, orgID, ¶ms.CurrentPeriodQuery) if err != nil { return nil, err } - p.logger.InfoContext(ctx, "fetching results for past period", "anomaly_past_period_query", params.PastPeriodQuery) + p.logger.InfoContext(ctx, "fetching results for past period", slog.Any("anomaly_past_period_query", params.PastPeriodQuery)) pastPeriodResults, err := p.querier.QueryRange(ctx, orgID, ¶ms.PastPeriodQuery) if err != nil { return nil, err } - p.logger.InfoContext(ctx, "fetching results for current season", "anomaly_current_season_query", params.CurrentSeasonQuery) + p.logger.InfoContext(ctx, "fetching results for current season", slog.Any("anomaly_current_season_query", params.CurrentSeasonQuery)) currentSeasonResults, err := p.querier.QueryRange(ctx, orgID, ¶ms.CurrentSeasonQuery) if err != nil { return nil, err } - p.logger.InfoContext(ctx, "fetching results for past season", "anomaly_past_season_query", params.PastSeasonQuery) + p.logger.InfoContext(ctx, "fetching results for past season", slog.Any("anomaly_past_season_query", params.PastSeasonQuery)) pastSeasonResults, err := p.querier.QueryRange(ctx, orgID, ¶ms.PastSeasonQuery) if err != nil { return nil, err } - p.logger.InfoContext(ctx, "fetching results for past 2 season", "anomaly_past_2season_query", params.Past2SeasonQuery) + p.logger.InfoContext(ctx, "fetching results for past 2 season", slog.Any("anomaly_past_2season_query", params.Past2SeasonQuery)) past2SeasonResults, err := p.querier.QueryRange(ctx, orgID, ¶ms.Past2SeasonQuery) if err != nil { return nil, err } - p.logger.InfoContext(ctx, "fetching results for past 3 season", "anomaly_past_3season_query", params.Past3SeasonQuery) + p.logger.InfoContext(ctx, "fetching results for past 3 season", slog.Any("anomaly_past_3season_query", params.Past3SeasonQuery)) past3SeasonResults, err := p.querier.QueryRange(ctx, orgID, ¶ms.Past3SeasonQuery) if err != nil { return nil, err @@ -212,17 +212,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 - p.logger.WarnContext(ctx, "predicted value is less than 0 for series", "anomaly_predicted_value", predictedValue, "anomaly_labels", series.Labels) + p.logger.WarnContext(ctx, "predicted value is less than 0 for series", slog.Float64("anomaly_predicted_value", predictedValue), slog.Any("anomaly_labels", series.Labels)) predictedValue = p.getMovingAvg(prevSeries, movingAvgWindowSize, idx) } p.logger.DebugContext(ctx, "predicted value for series", - "anomaly_moving_avg", movingAvg, - "anomaly_avg", avg, - "anomaly_mean", mean, - "anomaly_labels", series.Labels, - "anomaly_predicted_value", predictedValue, - "anomaly_curr", curr.Value, + slog.Float64("anomaly_moving_avg", movingAvg), + slog.Float64("anomaly_avg", avg), + slog.Float64("anomaly_mean", mean), + slog.Any("anomaly_labels", series.Labels), + slog.Float64("anomaly_predicted_value", predictedValue), + slog.Float64("anomaly_curr", curr.Value), ) predictedSeries.Values = append(predictedSeries.Values, &qbtypes.TimeSeriesValue{ Timestamp: curr.Timestamp, @@ -412,7 +412,7 @@ func (p *BaseSeasonalProvider) getAnomalies(ctx context.Context, orgID valuer.UU past3SeasonSeries := p.getMatchingSeries(ctx, past3SeasonResult, series) stdDev := p.getStdDev(currentSeasonSeries) - p.logger.InfoContext(ctx, "calculated standard deviation for series", "anomaly_std_dev", stdDev, "anomaly_labels", series.Labels) + p.logger.InfoContext(ctx, "calculated standard deviation for series", slog.Float64("anomaly_std_dev", stdDev), slog.Any("anomaly_labels", series.Labels)) prevSeriesAvg := p.getAvg(pastPeriodSeries) currentSeasonSeriesAvg := p.getAvg(currentSeasonSeries) @@ -420,12 +420,12 @@ func (p *BaseSeasonalProvider) getAnomalies(ctx context.Context, orgID valuer.UU past2SeasonSeriesAvg := p.getAvg(past2SeasonSeries) past3SeasonSeriesAvg := p.getAvg(past3SeasonSeries) p.logger.InfoContext(ctx, "calculated mean for series", - "anomaly_prev_series_avg", prevSeriesAvg, - "anomaly_current_season_series_avg", currentSeasonSeriesAvg, - "anomaly_past_season_series_avg", pastSeasonSeriesAvg, - "anomaly_past_2season_series_avg", past2SeasonSeriesAvg, - "anomaly_past_3season_series_avg", past3SeasonSeriesAvg, - "anomaly_labels", series.Labels, + slog.Float64("anomaly_prev_series_avg", prevSeriesAvg), + slog.Float64("anomaly_current_season_series_avg", currentSeasonSeriesAvg), + slog.Float64("anomaly_past_season_series_avg", pastSeasonSeriesAvg), + slog.Float64("anomaly_past_2season_series_avg", past2SeasonSeriesAvg), + slog.Float64("anomaly_past_3season_series_avg", past3SeasonSeriesAvg), + slog.Any("anomaly_labels", series.Labels), ) predictedSeries := p.getPredictedSeries( diff --git a/ee/authn/callbackauthn/oidccallbackauthn/authn.go b/ee/authn/callbackauthn/oidccallbackauthn/authn.go index 5efabbda741..b46690001a9 100644 --- a/ee/authn/callbackauthn/oidccallbackauthn/authn.go +++ b/ee/authn/callbackauthn/oidccallbackauthn/authn.go @@ -3,6 +3,7 @@ package oidccallbackauthn import ( "context" "fmt" + "log/slog" "net/url" "github.com/SigNoz/signoz/pkg/authn" @@ -150,7 +151,7 @@ func (a *AuthN) HandleCallback(ctx context.Context, query url.Values) (*authtype // Some IDPs return a single group as a string instead of an array groups = append(groups, g) default: - a.settings.Logger().WarnContext(ctx, "oidc: unsupported groups type", "type", fmt.Sprintf("%T", claimValue)) + a.settings.Logger().WarnContext(ctx, "oidc: unsupported groups type", slog.String("type", fmt.Sprintf("%T", claimValue))) } } } diff --git a/ee/licensing/httplicensing/provider.go b/ee/licensing/httplicensing/provider.go index 1258a29bf0c..edbe3e12460 100644 --- a/ee/licensing/httplicensing/provider.go +++ b/ee/licensing/httplicensing/provider.go @@ -3,8 +3,11 @@ package httplicensing import ( "context" "encoding/json" + "log/slog" "time" + "github.com/tidwall/gjson" + "github.com/SigNoz/signoz/ee/licensing/licensingstore/sqllicensingstore" "github.com/SigNoz/signoz/pkg/analytics" "github.com/SigNoz/signoz/pkg/errors" @@ -16,7 +19,6 @@ import ( "github.com/SigNoz/signoz/pkg/types/licensetypes" "github.com/SigNoz/signoz/pkg/valuer" "github.com/SigNoz/signoz/pkg/zeus" - "github.com/tidwall/gjson" ) type provider struct { @@ -55,7 +57,7 @@ func (provider *provider) Start(ctx context.Context) error { err := provider.Validate(ctx) if err != nil { - provider.settings.Logger().ErrorContext(ctx, "failed to validate license from upstream server", "error", err) + provider.settings.Logger().ErrorContext(ctx, "failed to validate license from upstream server", errors.Attr(err)) } for { @@ -65,7 +67,7 @@ func (provider *provider) Start(ctx context.Context) error { case <-tick.C: err := provider.Validate(ctx) if err != nil { - provider.settings.Logger().ErrorContext(ctx, "failed to validate license from upstream server", "error", err) + provider.settings.Logger().ErrorContext(ctx, "failed to validate license from upstream server", errors.Attr(err)) } } } @@ -133,7 +135,7 @@ func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUI if errors.Ast(err, errors.TypeNotFound) { return nil } - provider.settings.Logger().ErrorContext(ctx, "license validation failed", "org_id", organizationID.StringValue()) + provider.settings.Logger().ErrorContext(ctx, "license validation failed", slog.String("org_id", organizationID.StringValue())) return err } diff --git a/ee/querier/handler.go b/ee/querier/handler.go index c6f5dc4abfb..68a37996b93 100644 --- a/ee/querier/handler.go +++ b/ee/querier/handler.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "io" + "log/slog" "net/http" "runtime/debug" @@ -61,10 +62,10 @@ func (h *handler) QueryRange(rw http.ResponseWriter, req *http.Request) { queryJSON, _ := json.Marshal(queryRangeRequest) h.set.Logger.ErrorContext(ctx, "panic in QueryRange", - "error", r, - "user", claims.UserID, - "payload", string(queryJSON), - "stacktrace", stackTrace, + slog.Any("error", r), + slog.Any("user", claims.UserID), + slog.String("payload", string(queryJSON)), + slog.String("stacktrace", stackTrace), ) render.Error(rw, errors.NewInternalf( diff --git a/ee/query-service/app/api/featureFlags.go b/ee/query-service/app/api/featureFlags.go index dc2118f4219..4c5ebe3bb11 100644 --- a/ee/query-service/app/api/featureFlags.go +++ b/ee/query-service/app/api/featureFlags.go @@ -4,10 +4,15 @@ import ( "encoding/json" "errors" "fmt" + "io" "net/http" "time" + signozerrors "github.com/SigNoz/signoz/pkg/errors" + + "log/slog" + "github.com/SigNoz/signoz/ee/query-service/constants" "github.com/SigNoz/signoz/pkg/flagger" "github.com/SigNoz/signoz/pkg/http/render" @@ -15,7 +20,6 @@ import ( "github.com/SigNoz/signoz/pkg/types/featuretypes" "github.com/SigNoz/signoz/pkg/types/licensetypes" "github.com/SigNoz/signoz/pkg/valuer" - "log/slog" ) func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) { @@ -38,7 +42,7 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) { slog.DebugContext(ctx, "fetching license") license, err := ah.Signoz.Licensing.GetActive(ctx, orgID) if err != nil { - slog.ErrorContext(ctx, "failed to fetch license", "error", err) + slog.ErrorContext(ctx, "failed to fetch license", signozerrors.Attr(err)) } else if license == nil { slog.DebugContext(ctx, "no active license found") } else { @@ -51,7 +55,7 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) { // 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) + slog.ErrorContext(ctx, "failed to fetch zeus features", signozerrors.Attr(err)) } } } diff --git a/ee/query-service/app/api/queryrange.go b/ee/query-service/app/api/queryrange.go index 07b951dbb03..5fbee0f5bd8 100644 --- a/ee/query-service/app/api/queryrange.go +++ b/ee/query-service/app/api/queryrange.go @@ -7,6 +7,7 @@ import ( "net/http" "github.com/SigNoz/signoz/ee/query-service/anomaly" + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/http/render" baseapp "github.com/SigNoz/signoz/pkg/query-service/app" "github.com/SigNoz/signoz/pkg/query-service/app/queryBuilder" @@ -35,7 +36,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) + slog.ErrorContext(r.Context(), "error parsing metric query range params", errors.Attr(apiErrorObj.Err)) RespondError(w, apiErrorObj, nil) return } @@ -44,7 +45,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) + slog.ErrorContext(r.Context(), "error while adding temporality for metrics", errors.Attr(temporalityErr)) RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: temporalityErr}, nil) return } diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index f63784cfe78..845cb7b4268 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -8,16 +8,21 @@ import ( _ "net/http/pprof" // http profiler "slices" + "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux" + "go.opentelemetry.io/otel/propagation" + "github.com/SigNoz/signoz/pkg/cache/memorycache" + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/queryparser" "github.com/SigNoz/signoz/pkg/ruler/rulestore/sqlrulestore" "github.com/SigNoz/signoz/pkg/types/telemetrytypes" - "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux" - "go.opentelemetry.io/otel/propagation" "github.com/gorilla/handlers" + "github.com/rs/cors" + "github.com/soheilhy/cmux" + "github.com/SigNoz/signoz/ee/query-service/app/api" "github.com/SigNoz/signoz/ee/query-service/rules" "github.com/SigNoz/signoz/ee/query-service/usage" @@ -31,8 +36,8 @@ import ( "github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/telemetrystore" "github.com/SigNoz/signoz/pkg/web" - "github.com/rs/cors" - "github.com/soheilhy/cmux" + + "log/slog" "github.com/SigNoz/signoz/pkg/query-service/agentConf" baseapp "github.com/SigNoz/signoz/pkg/query-service/app" @@ -47,7 +52,6 @@ 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" ) // Server runs HTTP, Mux and a grpc server @@ -304,7 +308,7 @@ func (s *Server) Start(ctx context.Context) error { case nil, http.ErrServerClosed, cmux.ErrListenerClosed: // normal exit, nothing to do default: - slog.Error("Could not start HTTP server", "error", err) + slog.Error("Could not start HTTP server", errors.Attr(err)) } s.unavailableChannel <- healthcheck.Unavailable }() @@ -314,7 +318,7 @@ func (s *Server) Start(ctx context.Context) error { err = http.ListenAndServe(baseconst.DebugHttpPort, nil) if err != nil { - slog.Error("Could not start pprof server", "error", err) + slog.Error("Could not start pprof server", errors.Attr(err)) } }() @@ -322,7 +326,7 @@ func (s *Server) Start(ctx context.Context) error { slog.Info("Starting OpAmp Websocket server", "addr", baseconst.OpAmpWsEndpoint) err := s.opampServer.Start(baseconst.OpAmpWsEndpoint) if err != nil { - slog.Error("opamp ws server failed to start", "error", err) + slog.Error("opamp ws server failed to start", errors.Attr(err)) s.unavailableChannel <- healthcheck.Unavailable } }() diff --git a/ee/query-service/rules/anomaly.go b/ee/query-service/rules/anomaly.go index 8fd4432c38d..4a56e380294 100644 --- a/ee/query-service/rules/anomaly.go +++ b/ee/query-service/rules/anomaly.go @@ -12,6 +12,7 @@ import ( "github.com/SigNoz/signoz/ee/query-service/anomaly" "github.com/SigNoz/signoz/pkg/cache" + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/query-service/common" "github.com/SigNoz/signoz/pkg/query-service/model" "github.com/SigNoz/signoz/pkg/transition" @@ -308,7 +309,7 @@ func (r *AnomalyRule) buildAndRunQueryV5(ctx context.Context, orgID valuer.UUID, filteredSeries, filterErr := r.BaseRule.FilterNewSeries(ctx, ts, seriesToProcess) // In case of error we log the error and continue with the original series if filterErr != nil { - r.logger.ErrorContext(ctx, "Error filtering new series, ", "error", filterErr, "rule_name", r.Name()) + r.logger.ErrorContext(ctx, "Error filtering new series, ", errors.Attr(filterErr), "rule_name", r.Name()) } else { seriesToProcess = filteredSeries } @@ -391,7 +392,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (int, error) { result, err := tmpl.Expand() if err != nil { result = fmt.Sprintf("", err) - r.logger.ErrorContext(ctx, "Expanding alert template failed", "error", err, "data", tmplData, "rule_name", r.Name()) + r.logger.ErrorContext(ctx, "Expanding alert template failed", errors.Attr(err), "data", tmplData, "rule_name", r.Name()) } return result } @@ -467,7 +468,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (int, error) { for fp, a := range r.Active { labelsJSON, err := json.Marshal(a.QueryResultLables) if err != nil { - r.logger.ErrorContext(ctx, "error marshaling labels", "error", err, "labels", a.Labels) + r.logger.ErrorContext(ctx, "error marshaling labels", errors.Attr(err), "labels", a.Labels) } if _, ok := resultFPs[fp]; !ok { // If the alert was previously firing, keep it around for a given diff --git a/ee/query-service/rules/manager.go b/ee/query-service/rules/manager.go index 1f9b19e763a..a06e1ec6095 100644 --- a/ee/query-service/rules/manager.go +++ b/ee/query-service/rules/manager.go @@ -6,14 +6,16 @@ import ( "time" + "log/slog" + + "github.com/google/uuid" + "github.com/SigNoz/signoz/pkg/errors" basemodel "github.com/SigNoz/signoz/pkg/query-service/model" baserules "github.com/SigNoz/signoz/pkg/query-service/rules" "github.com/SigNoz/signoz/pkg/query-service/utils/labels" "github.com/SigNoz/signoz/pkg/types/ruletypes" "github.com/SigNoz/signoz/pkg/valuer" - "github.com/google/uuid" - "log/slog" ) func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error) { @@ -151,7 +153,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) + slog.Error("failed to prepare a new threshold rule for test", "name", alertname, errors.Attr(err)) return 0, basemodel.BadRequest(err) } @@ -173,7 +175,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) + slog.Error("failed to prepare a new promql rule for test", "name", alertname, errors.Attr(err)) return 0, basemodel.BadRequest(err) } } else if parsedRule.RuleType == ruletypes.RuleTypeAnomaly { @@ -193,7 +195,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) + slog.Error("failed to prepare a new anomaly rule for test", "name", alertname, errors.Attr(err)) return 0, basemodel.BadRequest(err) } } else { @@ -205,7 +207,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) + slog.Error("evaluating rule failed", "rule", rule.Name(), errors.Attr(err)) return 0, basemodel.InternalError(fmt.Errorf("rule evaluation failed")) } rule.SendAlerts(ctx, ts, 0, time.Minute, opts.NotifyFunc) diff --git a/ee/query-service/usage/manager.go b/ee/query-service/usage/manager.go index ec71b84378c..a10e7fee173 100644 --- a/ee/query-service/usage/manager.go +++ b/ee/query-service/usage/manager.go @@ -15,6 +15,7 @@ import ( "github.com/google/uuid" "github.com/SigNoz/signoz/ee/query-service/model" + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/licensing" "github.com/SigNoz/signoz/pkg/modules/organization" "github.com/SigNoz/signoz/pkg/query-service/utils/encryption" @@ -76,14 +77,14 @@ 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) + slog.ErrorContext(ctx, "failed to get organizations", errors.Attr(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) + slog.ErrorContext(ctx, "failed to get active license", errors.Attr(err)) return } if license == nil { @@ -115,7 +116,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) + slog.ErrorContext(ctx, "failed to get usage from clickhouse", errors.Attr(err)) return } for _, u := range dbusages { @@ -135,14 +136,14 @@ func (lm *Manager) UploadUsage(ctx context.Context) { 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) + slog.ErrorContext(ctx, "error while decrypting usage data", errors.Attr(err)) return } usageData := model.Usage{} err = json.Unmarshal(usageDataBytes, &usageData) if err != nil { - slog.ErrorContext(ctx, "error while unmarshalling usage data", "error", err) + slog.ErrorContext(ctx, "error while unmarshalling usage data", errors.Attr(err)) return } @@ -163,13 +164,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) + slog.ErrorContext(ctx, "error while marshalling usage payload", errors.Attr(errv2)) return } errv2 = lm.zeus.PutMeters(ctx, payload.LicenseKey.String(), body) if errv2 != nil { - slog.ErrorContext(ctx, "failed to upload usage", "error", errv2) + slog.ErrorContext(ctx, "failed to upload usage", errors.Attr(errv2)) // not returning error here since it is captured in the failed count return } diff --git a/ee/sqlschema/postgressqlschema/provider.go b/ee/sqlschema/postgressqlschema/provider.go index c063983ef86..f39c02fac38 100644 --- a/ee/sqlschema/postgressqlschema/provider.go +++ b/ee/sqlschema/postgressqlschema/provider.go @@ -4,11 +4,12 @@ import ( "context" "database/sql" + "github.com/uptrace/bun" + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/sqlschema" "github.com/SigNoz/signoz/pkg/sqlstore" - "github.com/uptrace/bun" ) type provider struct { @@ -113,7 +114,7 @@ WHERE defer func() { if err := constraintsRows.Close(); err != nil { - provider.settings.Logger().ErrorContext(ctx, "error closing rows", "error", err) + provider.settings.Logger().ErrorContext(ctx, "error closing rows", errors.Attr(err)) } }() @@ -174,7 +175,7 @@ WHERE defer func() { if err := foreignKeyConstraintsRows.Close(); err != nil { - provider.settings.Logger().ErrorContext(ctx, "error closing rows", "error", err) + provider.settings.Logger().ErrorContext(ctx, "error closing rows", errors.Attr(err)) } }() @@ -243,7 +244,7 @@ ORDER BY index_name, column_position`, string(name)) defer func() { if err := rows.Close(); err != nil { - provider.settings.Logger().ErrorContext(ctx, "error closing rows", "error", err) + provider.settings.Logger().ErrorContext(ctx, "error closing rows", errors.Attr(err)) } }() diff --git a/pkg/alertmanager/alertmanagerbatcher/batcher.go b/pkg/alertmanager/alertmanagerbatcher/batcher.go index 977321b014e..2e077a58992 100644 --- a/pkg/alertmanager/alertmanagerbatcher/batcher.go +++ b/pkg/alertmanager/alertmanagerbatcher/batcher.go @@ -87,14 +87,14 @@ func (batcher *Batcher) Add(ctx context.Context, alerts ...*alertmanagertypes.Po // batch could be. if d := len(alerts) - batcher.config.Capacity; d > 0 { alerts = alerts[d:] - batcher.logger.WarnContext(ctx, "alert batch larger than queue capacity, dropping alerts", "num_dropped", d, "capacity", batcher.config.Capacity) + batcher.logger.WarnContext(ctx, "alert batch larger than queue capacity, dropping alerts", slog.Int("num_dropped", d), slog.Int("capacity", batcher.config.Capacity)) } // If the queue is full, remove the oldest alerts in favor // of newer ones. if d := (len(batcher.queue) + len(alerts)) - batcher.config.Capacity; d > 0 { batcher.queue = batcher.queue[d:] - batcher.logger.WarnContext(ctx, "alert batch queue full, dropping alerts", "num_dropped", d) + batcher.logger.WarnContext(ctx, "alert batch queue full, dropping alerts", slog.Int("num_dropped", d)) } batcher.queue = append(batcher.queue, alerts...) diff --git a/pkg/alertmanager/alertmanagernotify/msteamsv2/msteamsv2.go b/pkg/alertmanager/alertmanagernotify/msteamsv2/msteamsv2.go index 257f0a4aa74..c1de2cda8e2 100644 --- a/pkg/alertmanager/alertmanagernotify/msteamsv2/msteamsv2.go +++ b/pkg/alertmanager/alertmanagernotify/msteamsv2/msteamsv2.go @@ -112,7 +112,7 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) return false, err } - n.logger.DebugContext(ctx, "extracted group key", "key", key) + n.logger.DebugContext(ctx, "extracted group key", slog.String("key", string(key))) data := notify.GetTemplateData(ctx, n.tmpl, as, n.logger) tmpl := notify.TmplText(n.tmpl, data, &err) diff --git a/pkg/alertmanager/alertmanagernotify/receiver.go b/pkg/alertmanager/alertmanagernotify/receiver.go index 33fd63f8245..77b93653f45 100644 --- a/pkg/alertmanager/alertmanagernotify/receiver.go +++ b/pkg/alertmanager/alertmanagernotify/receiver.go @@ -21,7 +21,7 @@ func NewReceiverIntegrations(nc alertmanagertypes.Receiver, tmpl *template.Templ errs types.MultiError integrations []notify.Integration add = func(name string, i int, rs notify.ResolvedSender, f func(l *slog.Logger) (notify.Notifier, error)) { - n, err := f(logger.With("integration", name)) + n, err := f(logger.With(slog.String("integration", name))) if err != nil { errs.Add(err) return diff --git a/pkg/alertmanager/alertmanagerserver/dispatcher.go b/pkg/alertmanager/alertmanagerserver/dispatcher.go index be2ace4b47d..ef4d03c6701 100644 --- a/pkg/alertmanager/alertmanagerserver/dispatcher.go +++ b/pkg/alertmanager/alertmanagerserver/dispatcher.go @@ -74,7 +74,7 @@ func NewDispatcher( route: r, marker: mk, timeout: to, - logger: l.With("component", "signoz-dispatcher"), + logger: l.With(slog.String("component", "signoz-dispatcher")), metrics: m, limits: lim, notificationManager: n, @@ -111,24 +111,24 @@ func (d *Dispatcher) run(it provider.AlertIterator) { if !ok || alertWrapper == nil { // Iterator exhausted for some reason. if err := it.Err(); err != nil { - d.logger.ErrorContext(d.ctx, "Error on alert update", "err", err) + d.logger.ErrorContext(d.ctx, "Error on alert update", errors.Attr(err)) } return } alert := alertWrapper.Data - d.logger.DebugContext(d.ctx, "SigNoz Custom Dispatcher: Received alert", "alert", alert) + d.logger.DebugContext(d.ctx, "SigNoz Custom Dispatcher: Received alert", slog.Any("alert", alert)) // Log errors but keep trying. if err := it.Err(); err != nil { - d.logger.ErrorContext(d.ctx, "Error on alert update", "err", err) + d.logger.ErrorContext(d.ctx, "Error on alert update", errors.Attr(err)) continue } now := time.Now() channels, err := d.notificationManager.Match(d.ctx, d.orgID, getRuleIDFromAlert(alert), alert.Labels) if err != nil { - d.logger.ErrorContext(d.ctx, "Error on alert match", "err", err) + d.logger.ErrorContext(d.ctx, "Error on alert match", errors.Attr(err)) continue } for _, channel := range channels { @@ -278,7 +278,7 @@ func (d *Dispatcher) processAlert(alert *types.Alert, route *dispatch.Route) { ruleId := getRuleIDFromAlert(alert) config, err := d.notificationManager.GetNotificationConfig(d.orgID, ruleId) if err != nil { - d.logger.ErrorContext(d.ctx, "error getting alert notification config", "rule_id", ruleId, "error", err) + d.logger.ErrorContext(d.ctx, "error getting alert notification config", slog.String("rule_id", ruleId), errors.Attr(err)) return } renotifyInterval := config.Renotify.RenotifyInterval @@ -310,7 +310,7 @@ func (d *Dispatcher) processAlert(alert *types.Alert, route *dispatch.Route) { // If the group does not exist, create it. But check the limit first. if limit := d.limits.MaxNumberOfAggregationGroups(); limit > 0 && d.aggrGroupsNum >= limit { d.metrics.aggrGroupLimitReached.Inc() - d.logger.ErrorContext(d.ctx, "Too many aggregation groups, cannot create new group for alert", "groups", d.aggrGroupsNum, "limit", limit, "alert", alert.Name()) + d.logger.ErrorContext(d.ctx, "Too many aggregation groups, cannot create new group for alert", slog.Int("groups", d.aggrGroupsNum), slog.Int("limit", limit), slog.String("alert", alert.Name())) return } @@ -328,7 +328,7 @@ func (d *Dispatcher) processAlert(alert *types.Alert, route *dispatch.Route) { go ag.run(func(ctx context.Context, alerts ...*types.Alert) bool { _, _, err := d.stage.Exec(ctx, d.logger, alerts...) if err != nil { - logger := d.logger.With("num_alerts", len(alerts), "err", err) + logger := d.logger.With(slog.Int("num_alerts", len(alerts)), errors.Attr(err)) if errors.Is(ctx.Err(), context.Canceled) { // It is expected for the context to be canceled on // configuration reload or shutdown. In this case, the @@ -382,7 +382,7 @@ func newAggrGroup(ctx context.Context, labels model.LabelSet, r *dispatch.Route, } ag.ctx, ag.cancel = context.WithCancel(ctx) - ag.logger = logger.With("aggr_group", ag) + ag.logger = logger.With(slog.Any("aggr_group", ag)) // Set an initial one-time wait before flushing // the first batch of notifications. @@ -457,7 +457,7 @@ func (ag *aggrGroup) stop() { // insert inserts the alert into the aggregation group. func (ag *aggrGroup) insert(alert *types.Alert) { if err := ag.alerts.Set(alert); err != nil { - ag.logger.ErrorContext(ag.ctx, "error on set alert", "err", err) + ag.logger.ErrorContext(ag.ctx, "error on set alert", errors.Attr(err)) } // Immediately trigger a flush if the wait duration for this @@ -497,7 +497,7 @@ func (ag *aggrGroup) flush(notify func(...*types.Alert) bool) { } sort.Stable(alertsSlice) - ag.logger.DebugContext(ag.ctx, "flushing", "alerts", fmt.Sprintf("%v", alertsSlice)) + ag.logger.DebugContext(ag.ctx, "flushing", slog.String("alerts", fmt.Sprintf("%v", alertsSlice))) if notify(alertsSlice...) { // Delete all resolved alerts as we just sent a notification for them, @@ -505,7 +505,7 @@ func (ag *aggrGroup) flush(notify func(...*types.Alert) bool) { // that each resolved alert has not fired again during the flush as then // we would delete an active alert thinking it was resolved. if err := ag.alerts.DeleteIfNotModified(resolvedSlice); err != nil { - ag.logger.ErrorContext(ag.ctx, "error on delete alerts", "err", err) + ag.logger.ErrorContext(ag.ctx, "error on delete alerts", errors.Attr(err)) } } } diff --git a/pkg/alertmanager/alertmanagerserver/server.go b/pkg/alertmanager/alertmanagerserver/server.go index 5cc96e397e4..bd1c85d33a8 100644 --- a/pkg/alertmanager/alertmanagerserver/server.go +++ b/pkg/alertmanager/alertmanagerserver/server.go @@ -10,10 +10,6 @@ import ( "github.com/prometheus/alertmanager/types" "golang.org/x/sync/errgroup" - "github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify" - "github.com/SigNoz/signoz/pkg/alertmanager/nfmanager" - "github.com/SigNoz/signoz/pkg/errors" - "github.com/SigNoz/signoz/pkg/types/alertmanagertypes" "github.com/prometheus/alertmanager/dispatch" "github.com/prometheus/alertmanager/featurecontrol" "github.com/prometheus/alertmanager/inhibit" @@ -25,6 +21,11 @@ import ( "github.com/prometheus/alertmanager/timeinterval" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/model" + + "github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify" + "github.com/SigNoz/signoz/pkg/alertmanager/nfmanager" + "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/types/alertmanagertypes" ) var ( @@ -72,7 +73,7 @@ type Server struct { func New(ctx context.Context, logger *slog.Logger, registry prometheus.Registerer, srvConfig Config, orgID string, stateStore alertmanagertypes.StateStore, nfManager nfmanager.NotificationManager) (*Server, error) { server := &Server{ - logger: logger.With("pkg", "go.signoz.io/pkg/alertmanager/alertmanagerserver"), + logger: logger.With(slog.String("pkg", "go.signoz.io/pkg/alertmanager/alertmanagerserver")), registry: registry, srvConfig: srvConfig, orgID: orgID, @@ -139,7 +140,7 @@ func New(ctx context.Context, logger *slog.Logger, registry prometheus.Registere server.silences.Maintenance(server.srvConfig.Silences.MaintenanceInterval, snapfnoop, server.stopc, func() (int64, error) { // Delete silences older than the retention period. if _, err := server.silences.GC(); err != nil { - server.logger.ErrorContext(ctx, "silence garbage collection", "error", err) + server.logger.ErrorContext(ctx, "silence garbage collection", errors.Attr(err)) // Don't return here - we need to snapshot our state first. } @@ -168,7 +169,7 @@ func New(ctx context.Context, logger *slog.Logger, registry prometheus.Registere defer server.wg.Done() server.nflog.Maintenance(server.srvConfig.NFLog.MaintenanceInterval, snapfnoop, server.stopc, func() (int64, error) { if _, err := server.nflog.GC(); err != nil { - server.logger.ErrorContext(ctx, "notification log garbage collection", "error", err) + server.logger.ErrorContext(ctx, "notification log garbage collection", errors.Attr(err)) // Don't return without saving the current state. } @@ -246,7 +247,7 @@ func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertma for _, rcv := range config.Receivers { if _, found := activeReceivers[rcv.Name]; !found { // No need to build a receiver if no route is using it. - server.logger.InfoContext(ctx, "skipping creation of receiver not referenced by any route", "receiver", rcv.Name) + server.logger.InfoContext(ctx, "skipping creation of receiver not referenced by any route", slog.String("receiver", rcv.Name)) continue } integrations, err := alertmanagernotify.NewReceiverIntegrations(rcv, server.tmpl, server.logger) diff --git a/pkg/alertmanager/nfmanager/rulebasednotification/provider.go b/pkg/alertmanager/nfmanager/rulebasednotification/provider.go index 45239fb2ec4..e487304b66c 100644 --- a/pkg/alertmanager/nfmanager/rulebasednotification/provider.go +++ b/pkg/alertmanager/nfmanager/rulebasednotification/provider.go @@ -2,6 +2,7 @@ package rulebasednotification import ( "context" + "log/slog" "strings" "sync" @@ -266,7 +267,7 @@ func (r *provider) convertLabelSetToEnv(ctx context.Context, labelSet model.Labe } if logForReview { - r.settings.Logger().InfoContext(ctx, "found label set with conflicting prefix dotted keys", "labels", labelSet) + r.settings.Logger().InfoContext(ctx, "found label set with conflicting prefix dotted keys", slog.Any("labels", labelSet)) } return env diff --git a/pkg/alertmanager/service.go b/pkg/alertmanager/service.go index 47cd0a4ce95..471b4de8655 100644 --- a/pkg/alertmanager/service.go +++ b/pkg/alertmanager/service.go @@ -2,6 +2,7 @@ package alertmanager import ( "context" + "log/slog" "sync" "github.com/prometheus/alertmanager/featurecontrol" @@ -74,7 +75,7 @@ func (service *Service) SyncServers(ctx context.Context) error { for _, org := range orgs { config, _, err := service.getConfig(ctx, org.ID.StringValue()) if err != nil { - service.settings.Logger().ErrorContext(ctx, "failed to get alertmanager config for org", "org_id", org.ID.StringValue(), "error", err) + service.settings.Logger().ErrorContext(ctx, "failed to get alertmanager config for org", slog.String("org_id", org.ID.StringValue()), errors.Attr(err)) continue } @@ -82,7 +83,7 @@ func (service *Service) SyncServers(ctx context.Context) error { if _, ok := service.servers[org.ID.StringValue()]; !ok { server, err := service.newServer(ctx, org.ID.StringValue()) if err != nil { - service.settings.Logger().ErrorContext(ctx, "failed to create alertmanager server", "org_id", org.ID.StringValue(), "error", err) + service.settings.Logger().ErrorContext(ctx, "failed to create alertmanager server", slog.String("org_id", org.ID.StringValue()), errors.Attr(err)) continue } @@ -90,13 +91,13 @@ func (service *Service) SyncServers(ctx context.Context) error { } if service.servers[org.ID.StringValue()].Hash() == config.StoreableConfig().Hash { - service.settings.Logger().DebugContext(ctx, "skipping alertmanager sync for org", "org_id", org.ID.StringValue(), "hash", config.StoreableConfig().Hash) + service.settings.Logger().DebugContext(ctx, "skipping alertmanager sync for org", slog.String("org_id", org.ID.StringValue()), slog.String("hash", config.StoreableConfig().Hash)) continue } err = service.servers[org.ID.StringValue()].SetConfig(ctx, config) if err != nil { - service.settings.Logger().ErrorContext(ctx, "failed to set config for alertmanager server", "org_id", org.ID.StringValue(), "error", err) + service.settings.Logger().ErrorContext(ctx, "failed to set config for alertmanager server", slog.String("org_id", org.ID.StringValue()), errors.Attr(err)) continue } } @@ -163,7 +164,7 @@ func (service *Service) Stop(ctx context.Context) error { for _, server := range service.servers { if err := server.Stop(ctx); err != nil { errs = append(errs, err) - service.settings.Logger().ErrorContext(ctx, "failed to stop alertmanager server", "error", err) + service.settings.Logger().ErrorContext(ctx, "failed to stop alertmanager server", errors.Attr(err)) } } @@ -191,7 +192,7 @@ func (service *Service) newServer(ctx context.Context, orgID string) (*alertmana // defaults from an upstream upgrade or something similar) trigger a DB update // so that other code paths reading directly from the store see the up-to-date config. if storedHash == config.StoreableConfig().Hash { - service.settings.Logger().DebugContext(ctx, "skipping config store update for org", "org_id", orgID, "hash", config.StoreableConfig().Hash) + service.settings.Logger().DebugContext(ctx, "skipping config store update for org", slog.String("org_id", orgID), slog.String("hash", config.StoreableConfig().Hash)) return server, nil } diff --git a/pkg/alertmanager/signozalertmanager/provider.go b/pkg/alertmanager/signozalertmanager/provider.go index 8160413d191..1a358130837 100644 --- a/pkg/alertmanager/signozalertmanager/provider.go +++ b/pkg/alertmanager/signozalertmanager/provider.go @@ -4,9 +4,10 @@ import ( "context" "time" - "github.com/SigNoz/signoz/pkg/query-service/utils/labels" "github.com/prometheus/common/model" + "github.com/SigNoz/signoz/pkg/query-service/utils/labels" + amConfig "github.com/prometheus/alertmanager/config" "github.com/SigNoz/signoz/pkg/alertmanager" @@ -66,7 +67,7 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config func (provider *provider) Start(ctx context.Context) error { if err := provider.service.SyncServers(ctx); err != nil { - provider.settings.Logger().ErrorContext(ctx, "failed to sync alertmanager servers", "error", err) + provider.settings.Logger().ErrorContext(ctx, "failed to sync alertmanager servers", errors.Attr(err)) return err } @@ -78,7 +79,7 @@ func (provider *provider) Start(ctx context.Context) error { return nil case <-ticker.C: if err := provider.service.SyncServers(ctx); err != nil { - provider.settings.Logger().ErrorContext(ctx, "failed to sync alertmanager servers", "error", err) + provider.settings.Logger().ErrorContext(ctx, "failed to sync alertmanager servers", errors.Attr(err)) } } } diff --git a/pkg/analytics/segmentanalytics/provider.go b/pkg/analytics/segmentanalytics/provider.go index bd20621d0d5..671f877ac3c 100644 --- a/pkg/analytics/segmentanalytics/provider.go +++ b/pkg/analytics/segmentanalytics/provider.go @@ -2,8 +2,10 @@ package segmentanalytics import ( "context" + "log/slog" "github.com/SigNoz/signoz/pkg/analytics" + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/types/analyticstypes" segment "github.com/segmentio/analytics-go/v3" @@ -45,14 +47,14 @@ func (provider *provider) Send(ctx context.Context, messages ...analyticstypes.M for _, message := range messages { err := provider.client.Enqueue(message) if err != nil { - provider.settings.Logger().WarnContext(ctx, "unable to send message to segment", "err", err) + provider.settings.Logger().WarnContext(ctx, "unable to send message to segment", errors.Attr(err)) } } } func (provider *provider) TrackGroup(ctx context.Context, group, event string, properties map[string]any) { if properties == nil { - provider.settings.Logger().WarnContext(ctx, "empty attributes received, skipping event", "group", group, "event", event) + provider.settings.Logger().WarnContext(ctx, "empty attributes received, skipping event", slog.String("group", group), slog.String("event", event)) return } @@ -67,13 +69,13 @@ func (provider *provider) TrackGroup(ctx context.Context, group, event string, p }, }) if err != nil { - provider.settings.Logger().WarnContext(ctx, "unable to send message to segment", "err", err) + provider.settings.Logger().WarnContext(ctx, "unable to send message to segment", errors.Attr(err)) } } func (provider *provider) TrackUser(ctx context.Context, group, user, event string, properties map[string]any) { if properties == nil { - provider.settings.Logger().WarnContext(ctx, "empty attributes received, skipping event", "user", user, "group", group, "event", event) + provider.settings.Logger().WarnContext(ctx, "empty attributes received, skipping event", slog.String("user", user), slog.String("group", group), slog.String("event", event)) return } @@ -88,13 +90,13 @@ func (provider *provider) TrackUser(ctx context.Context, group, user, event stri }, }) if err != nil { - provider.settings.Logger().WarnContext(ctx, "unable to send message to segment", "err", err) + provider.settings.Logger().WarnContext(ctx, "unable to send message to segment", errors.Attr(err)) } } func (provider *provider) IdentifyGroup(ctx context.Context, group string, traits map[string]any) { if traits == nil { - provider.settings.Logger().WarnContext(ctx, "empty attributes received, skipping identify", "group", group) + provider.settings.Logger().WarnContext(ctx, "empty attributes received, skipping identify", slog.String("group", group)) return } @@ -104,7 +106,7 @@ func (provider *provider) IdentifyGroup(ctx context.Context, group string, trait Traits: analyticstypes.NewTraitsFromMap(traits), }) if err != nil { - provider.settings.Logger().WarnContext(ctx, "unable to send message to segment", "err", err) + provider.settings.Logger().WarnContext(ctx, "unable to send message to segment", errors.Attr(err)) } // identify the group using the stats user @@ -114,13 +116,13 @@ func (provider *provider) IdentifyGroup(ctx context.Context, group string, trait Traits: analyticstypes.NewTraitsFromMap(traits), }) if err != nil { - provider.settings.Logger().WarnContext(ctx, "unable to send message to segment", "err", err) + provider.settings.Logger().WarnContext(ctx, "unable to send message to segment", errors.Attr(err)) } } func (provider *provider) IdentifyUser(ctx context.Context, group, user string, traits map[string]any) { if traits == nil { - provider.settings.Logger().WarnContext(ctx, "empty attributes received, skipping identify", "user", user, "group", group) + provider.settings.Logger().WarnContext(ctx, "empty attributes received, skipping identify", slog.String("user", user), slog.String("group", group)) return } @@ -130,7 +132,7 @@ func (provider *provider) IdentifyUser(ctx context.Context, group, user string, Traits: analyticstypes.NewTraitsFromMap(traits), }) if err != nil { - provider.settings.Logger().WarnContext(ctx, "unable to send message to segment", "err", err) + provider.settings.Logger().WarnContext(ctx, "unable to send message to segment", errors.Attr(err)) } // associate the user with the group @@ -140,13 +142,13 @@ func (provider *provider) IdentifyUser(ctx context.Context, group, user string, Traits: analyticstypes.NewTraits().Set("id", group), // A trait is required }) if err != nil { - provider.settings.Logger().WarnContext(ctx, "unable to send message to segment", "err", err) + provider.settings.Logger().WarnContext(ctx, "unable to send message to segment", errors.Attr(err)) } } func (provider *provider) Stop(ctx context.Context) error { if err := provider.client.Close(); err != nil { - provider.settings.Logger().WarnContext(ctx, "unable to close segment client", "err", err) + provider.settings.Logger().WarnContext(ctx, "unable to close segment client", errors.Attr(err)) } close(provider.stopC) diff --git a/pkg/authn/callbackauthn/googlecallbackauthn/authn.go b/pkg/authn/callbackauthn/googlecallbackauthn/authn.go index 8293d5e4d80..2f04ffd5289 100644 --- a/pkg/authn/callbackauthn/googlecallbackauthn/authn.go +++ b/pkg/authn/callbackauthn/googlecallbackauthn/authn.go @@ -2,19 +2,21 @@ package googlecallbackauthn import ( "context" + "log/slog" "net/url" + "github.com/coreos/go-oidc/v3/oidc" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + admin "google.golang.org/api/admin/directory/v1" + "google.golang.org/api/option" + "github.com/SigNoz/signoz/pkg/authn" "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/http/client" "github.com/SigNoz/signoz/pkg/types/authtypes" "github.com/SigNoz/signoz/pkg/valuer" - "github.com/coreos/go-oidc/v3/oidc" - "golang.org/x/oauth2" - "golang.org/x/oauth2/google" - admin "google.golang.org/api/admin/directory/v1" - "google.golang.org/api/option" ) const ( @@ -72,13 +74,13 @@ func (a *AuthN) HandleCallback(ctx context.Context, query url.Values) (*authtype } if err := query.Get("error"); err != "" { - a.settings.Logger().ErrorContext(ctx, "google: error while authenticating", "error", err, "error_description", query.Get("error_description")) + a.settings.Logger().ErrorContext(ctx, "google: error while authenticating", slog.String("error", err), slog.String("error_description", query.Get("error_description"))) return nil, errors.Newf(errors.TypeInternal, errors.CodeInternal, "google: error while authenticating").WithAdditional(query.Get("error_description")) } state, err := authtypes.NewStateFromString(query.Get("state")) if err != nil { - a.settings.Logger().ErrorContext(ctx, "google: invalid state", "error", err) + a.settings.Logger().ErrorContext(ctx, "google: invalid state", errors.Attr(err)) return nil, errors.Newf(errors.TypeInvalidInput, authtypes.ErrCodeInvalidState, "google: invalid state").WithAdditional(err.Error()) } @@ -92,11 +94,11 @@ func (a *AuthN) HandleCallback(ctx context.Context, query url.Values) (*authtype if err != nil { var retrieveError *oauth2.RetrieveError if errors.As(err, &retrieveError) { - a.settings.Logger().ErrorContext(ctx, "google: failed to get token", "error", err, "error_description", retrieveError.ErrorDescription, "body", string(retrieveError.Body)) + a.settings.Logger().ErrorContext(ctx, "google: failed to get token", errors.Attr(err), slog.String("error_description", retrieveError.ErrorDescription), slog.String("body", string(retrieveError.Body))) return nil, errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "google: failed to get token").WithAdditional(retrieveError.ErrorDescription) } - a.settings.Logger().ErrorContext(ctx, "google: failed to get token", "error", err) + a.settings.Logger().ErrorContext(ctx, "google: failed to get token", errors.Attr(err)) return nil, errors.Newf(errors.TypeInternal, errors.CodeInternal, "google: failed to get token") } @@ -108,7 +110,7 @@ func (a *AuthN) HandleCallback(ctx context.Context, query url.Values) (*authtype verifier := oidcProvider.Verifier(&oidc.Config{ClientID: authDomain.AuthDomainConfig().Google.ClientID}) idToken, err := verifier.Verify(ctx, rawIDToken) if err != nil { - a.settings.Logger().ErrorContext(ctx, "google: failed to verify token", "error", err) + a.settings.Logger().ErrorContext(ctx, "google: failed to verify token", errors.Attr(err)) return nil, errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "google: failed to verify token") } @@ -120,18 +122,18 @@ func (a *AuthN) HandleCallback(ctx context.Context, query url.Values) (*authtype } if err := idToken.Claims(&claims); err != nil { - a.settings.Logger().ErrorContext(ctx, "google: missing or invalid claims", "error", err) + a.settings.Logger().ErrorContext(ctx, "google: missing or invalid claims", errors.Attr(err)) return nil, errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "google: missing or invalid claims").WithAdditional(err.Error()) } if claims.HostedDomain != authDomain.StorableAuthDomain().Name { - a.settings.Logger().ErrorContext(ctx, "google: unexpected hd claim", "expected", authDomain.StorableAuthDomain().Name, "actual", claims.HostedDomain) + a.settings.Logger().ErrorContext(ctx, "google: unexpected hd claim", slog.String("expected", authDomain.StorableAuthDomain().Name), slog.String("actual", claims.HostedDomain)) return nil, errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "google: unexpected hd claim") } if !authDomain.AuthDomainConfig().Google.InsecureSkipEmailVerified { if !claims.EmailVerified { - a.settings.Logger().ErrorContext(ctx, "google: email is not verified", "email", claims.Email) + a.settings.Logger().ErrorContext(ctx, "google: email is not verified", slog.String("email", claims.Email)) return nil, errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "google: email is not verified") } } @@ -145,7 +147,7 @@ func (a *AuthN) HandleCallback(ctx context.Context, query url.Values) (*authtype if authDomain.AuthDomainConfig().Google.FetchGroups { groups, err = a.fetchGoogleWorkspaceGroups(ctx, claims.Email, authDomain.AuthDomainConfig().Google) if err != nil { - a.settings.Logger().ErrorContext(ctx, "google: could not fetch groups", "error", err) + a.settings.Logger().ErrorContext(ctx, "google: could not fetch groups", errors.Attr(err)) return nil, errors.Newf(errors.TypeInternal, errors.CodeInternal, "google: could not fetch groups").WithAdditional(err.Error()) } @@ -189,7 +191,7 @@ func (a *AuthN) fetchGoogleWorkspaceGroups(ctx context.Context, userEmail string jwtConfig, err := google.JWTConfigFromJSON([]byte(config.ServiceAccountJSON), admin.AdminDirectoryGroupReadonlyScope) if err != nil { - a.settings.Logger().ErrorContext(ctx, "google: invalid service account credentials", "error", err) + a.settings.Logger().ErrorContext(ctx, "google: invalid service account credentials", errors.Attr(err)) return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid service account credentials") } @@ -199,7 +201,7 @@ func (a *AuthN) fetchGoogleWorkspaceGroups(ctx context.Context, userEmail string adminService, err := admin.NewService(ctx, option.WithHTTPClient(jwtConfig.Client(customCtx))) if err != nil { - a.settings.Logger().ErrorContext(ctx, "google: unable to create directory service", "error", err) + a.settings.Logger().ErrorContext(ctx, "google: unable to create directory service", errors.Attr(err)) return nil, errors.Newf(errors.TypeInternal, errors.CodeInternal, "unable to create directory service") } @@ -221,7 +223,7 @@ func (a *AuthN) getGroups(ctx context.Context, adminService *admin.Service, user groupList, err := call.Context(ctx).Do() if err != nil { - a.settings.Logger().ErrorContext(ctx, "google: unable to list groups", "error", err) + a.settings.Logger().ErrorContext(ctx, "google: unable to list groups", errors.Attr(err)) return nil, errors.Newf(errors.TypeInternal, errors.CodeInternal, "unable to list groups") } @@ -236,7 +238,7 @@ func (a *AuthN) getGroups(ctx context.Context, adminService *admin.Service, user if fetchTransitive { transitiveGroups, err := a.getGroups(ctx, adminService, group.Email, fetchTransitive, checkedGroups) if err != nil { - a.settings.Logger().ErrorContext(ctx, "google: unable to list transitive groups", "error", err) + a.settings.Logger().ErrorContext(ctx, "google: unable to list transitive groups", errors.Attr(err)) return nil, errors.Newf(errors.TypeInternal, errors.CodeInternal, "unable to list transitive groups") } userGroups = append(userGroups, transitiveGroups...) diff --git a/pkg/cache/rediscache/provider.go b/pkg/cache/rediscache/provider.go index 0c38fb6b82d..383c1864891 100644 --- a/pkg/cache/rediscache/provider.go +++ b/pkg/cache/rediscache/provider.go @@ -2,18 +2,19 @@ package rediscache import ( "context" + "fmt" + "log/slog" "strings" "time" - "fmt" + "github.com/redis/go-redis/extra/redisotel/v9" + "github.com/redis/go-redis/v9" "github.com/SigNoz/signoz/pkg/cache" "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/types/cachetypes" "github.com/SigNoz/signoz/pkg/valuer" - "github.com/redis/go-redis/extra/redisotel/v9" - "github.com/redis/go-redis/v9" ) type provider struct { @@ -76,6 +77,6 @@ func (c *provider) DeleteMany(ctx context.Context, orgID valuer.UUID, cacheKeys } if err := c.client.Del(ctx, updatedCacheKeys...).Err(); err != nil { - c.settings.Logger().ErrorContext(ctx, "error deleting cache keys", "cache_keys", cacheKeys, "error", err) + c.settings.Logger().ErrorContext(ctx, "error deleting cache keys", slog.Any("cache_keys", cacheKeys), errors.Attr(err)) } } diff --git a/pkg/emailing/noopemailing/provider.go b/pkg/emailing/noopemailing/provider.go index 9384487cf69..e7853a88244 100644 --- a/pkg/emailing/noopemailing/provider.go +++ b/pkg/emailing/noopemailing/provider.go @@ -2,6 +2,7 @@ package noopemailing import ( "context" + "log/slog" "github.com/SigNoz/signoz/pkg/emailing" "github.com/SigNoz/signoz/pkg/factory" @@ -24,6 +25,6 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config } func (provider *provider) SendHTML(ctx context.Context, to string, subject string, templateName emailtypes.TemplateName, data map[string]any) error { - provider.settings.Logger().WarnContext(ctx, "using noop provider, no email will be sent", "to", to, "subject", subject) + provider.settings.Logger().WarnContext(ctx, "using noop provider, no email will be sent", slog.String("to", to), slog.String("subject", subject)) return nil } diff --git a/pkg/emailing/smtpemailing/provider.go b/pkg/emailing/smtpemailing/provider.go index 07afacecf3a..1cf3bbc402a 100644 --- a/pkg/emailing/smtpemailing/provider.go +++ b/pkg/emailing/smtpemailing/provider.go @@ -6,6 +6,7 @@ import ( "github.com/SigNoz/signoz/pkg/emailing" "github.com/SigNoz/signoz/pkg/emailing/templatestore/filetemplatestore" + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/smtp/client" "github.com/SigNoz/signoz/pkg/types/emailtypes" @@ -28,7 +29,7 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config // Try to create a template store. If it fails, use an empty store. store, err := filetemplatestore.NewStore(ctx, config.Templates.Directory, emailtypes.Templates, settings.Logger()) if err != nil { - settings.Logger().ErrorContext(ctx, "failed to create template store, using empty store", "error", err) + settings.Logger().ErrorContext(ctx, "failed to create template store, using empty store", errors.Attr(err)) store = filetemplatestore.NewEmptyStore() } @@ -87,7 +88,7 @@ func (provider *provider) SendHTML(ctx context.Context, to string, subject strin content, err := emailtypes.NewContent(template, data) if err != nil { - provider.settings.Logger().ErrorContext(ctx, "failed to create email content", "error", err) + provider.settings.Logger().ErrorContext(ctx, "failed to create email content", errors.Attr(err)) return err } diff --git a/pkg/emailing/templatestore/filetemplatestore/store.go b/pkg/emailing/templatestore/filetemplatestore/store.go index d79a2492470..e9f67bdfc46 100644 --- a/pkg/emailing/templatestore/filetemplatestore/store.go +++ b/pkg/emailing/templatestore/filetemplatestore/store.go @@ -45,7 +45,7 @@ func NewStore(ctx context.Context, baseDir string, templates []emailtypes.Templa t, err := parseTemplateFile(filepath.Join(baseDir, fi.Name()), templateName) if err != nil { - logger.ErrorContext(ctx, "failed to parse template file", "template", templateName, "path", filepath.Join(baseDir, fi.Name()), "error", err) + logger.ErrorContext(ctx, "failed to parse template file", slog.Any("template", templateName), slog.String("path", filepath.Join(baseDir, fi.Name())), errors.Attr(err)) continue } @@ -54,7 +54,7 @@ func NewStore(ctx context.Context, baseDir string, templates []emailtypes.Templa } if err := checkMissingTemplates(templates, foundTemplates); err != nil { - logger.ErrorContext(ctx, "some templates are missing", "error", err) + logger.ErrorContext(ctx, "some templates are missing", errors.Attr(err)) } return &store{fs: fs}, nil diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go index 26f7e365a6f..6a1a3089475 100644 --- a/pkg/errors/errors.go +++ b/pkg/errors/errors.go @@ -7,7 +7,7 @@ import ( ) // base is the fundamental struct that implements the error interface. -// The order of the struct is 'TCMEUA'. +// The order of the struct is 'TCMEUAS'. type base struct { // t denotes the custom type of the error. t typ @@ -21,16 +21,13 @@ type base struct { u string // a denotes any additional error messages (if present). a []string + // s contains the stacktrace captured at error creation time. + s stacktrace } -func (b *base) LogValue() slog.Value { - return slog.GroupValue( - slog.String("type", b.t.s), - slog.String("code", b.c.s), - slog.String("message", b.m), - slog.String("url", b.u), - slog.Any("additional", b.a), - ) +// Stacktrace returns the stacktrace captured at error creation time, formatted as a string. +func (b *base) Stacktrace() string { + return b.s.String() } // base implements Error interface. @@ -51,6 +48,7 @@ func New(t typ, code Code, message string) *base { e: nil, u: "", a: []string{}, + s: newStackTrace(), } } @@ -61,6 +59,7 @@ func Newf(t typ, code Code, format string, args ...any) *base { c: code, m: fmt.Sprintf(format, args...), e: nil, + s: newStackTrace(), } } @@ -72,6 +71,7 @@ func Wrapf(cause error, t typ, code Code, format string, args ...any) *base { c: code, m: fmt.Sprintf(format, args...), e: cause, + s: newStackTrace(), } } @@ -82,12 +82,17 @@ func Wrap(cause error, t typ, code Code, message string) *base { c: code, m: message, e: cause, + s: newStackTrace(), } } // WithAdditionalf adds an additional error message to the existing error. func WithAdditionalf(cause error, format string, args ...any) *base { t, c, m, e, u, a := Unwrapb(cause) + var s stacktrace + if original, ok := cause.(*base); ok { + s = original.s + } b := &base{ t: t, c: c, @@ -95,6 +100,7 @@ func WithAdditionalf(cause error, format string, args ...any) *base { e: e, u: u, a: a, + s: s, } return b.WithAdditional(append(a, fmt.Sprintf(format, args...))...) @@ -109,6 +115,7 @@ func (b *base) WithUrl(u string) *base { e: b.e, u: u, a: b.a, + s: b.s, } } @@ -121,6 +128,7 @@ func (b *base) WithAdditional(a ...string) *base { e: b.e, u: b.u, a: a, + s: b.s, } } @@ -223,3 +231,8 @@ func WrapTimeoutf(cause error, code Code, format string, args ...any) *base { func NewTimeoutf(code Code, format string, args ...any) *base { return Newf(TypeTimeout, code, format, args...) } + +// Attr returns an slog.Attr with a standardized "exception" key for the given error. +func Attr(err error) slog.Attr { + return slog.Any("exception", err) +} diff --git a/pkg/errors/errors_test.go b/pkg/errors/errors_test.go index 1f04c24f089..d07329892c0 100644 --- a/pkg/errors/errors_test.go +++ b/pkg/errors/errors_test.go @@ -51,3 +51,10 @@ func TestUnwrapb(t *testing.T) { atyp, _, _, _, _, _ = Unwrapb(oerr) assert.Equal(t, TypeInternal, atyp) } + +func TestAttr(t *testing.T) { + err := New(TypeInternal, MustNewCode("test_code"), "test error") + attr := Attr(err) + assert.Equal(t, "exception", attr.Key) + assert.Equal(t, err, attr.Value.Any()) +} diff --git a/pkg/errors/stacktrace.go b/pkg/errors/stacktrace.go new file mode 100644 index 00000000000..6a6ddc61243 --- /dev/null +++ b/pkg/errors/stacktrace.go @@ -0,0 +1,38 @@ +package errors + +import ( + "fmt" + "runtime" + "strings" +) + +// stacktrace holds a snapshot of program counters. +type stacktrace []uintptr + +// newStackTrace captures a stack trace, skipping 3 frames to record the +// snapshot at the origin of the error: +// 1. runtime.Callers +// 2. newStackTrace +// 3. the constructor (New, Newf, Wrapf, Wrap) +// +// Inspired by https://github.com/thanos-io/thanos/blob/main/pkg/errors/stacktrace.go +func newStackTrace() stacktrace { + const depth = 16 + pc := make([]uintptr, depth) + n := runtime.Callers(3, pc) + return stacktrace(pc[:n:n]) +} + +// String formats the stacktrace as function/file/line pairs. +func (s stacktrace) String() string { + var buf strings.Builder + frames := runtime.CallersFrames(s) + for { + frame, more := frames.Next() + fmt.Fprintf(&buf, "%s\n\t%s:%d\n", frame.Function, frame.File, frame.Line) + if !more { + break + } + } + return buf.String() +} diff --git a/pkg/factory/registry.go b/pkg/factory/registry.go index 80fdba13c4e..23df73c1d61 100644 --- a/pkg/factory/registry.go +++ b/pkg/factory/registry.go @@ -37,7 +37,7 @@ func NewRegistry(logger *slog.Logger, services ...NamedService) (*Registry, erro } return &Registry{ - logger: logger.With("pkg", "go.signoz.io/pkg/factory"), + logger: logger.With(slog.String("pkg", "go.signoz.io/pkg/factory")), services: m, startCh: make(chan error, 1), stopCh: make(chan error, len(services)), @@ -47,7 +47,7 @@ func NewRegistry(logger *slog.Logger, services ...NamedService) (*Registry, erro func (r *Registry) Start(ctx context.Context) { for _, s := range r.services.GetInOrder() { go func(s NamedService) { - r.logger.InfoContext(ctx, "starting service", "service", s.Name()) + r.logger.InfoContext(ctx, "starting service", slog.Any("service", s.Name())) err := s.Start(ctx) r.startCh <- err }(s) @@ -61,11 +61,11 @@ func (r *Registry) Wait(ctx context.Context) error { select { case <-ctx.Done(): - r.logger.InfoContext(ctx, "caught context error, exiting", "error", ctx.Err()) + r.logger.InfoContext(ctx, "caught context error, exiting", errors.Attr(ctx.Err())) case s := <-interrupt: - r.logger.InfoContext(ctx, "caught interrupt signal, exiting", "signal", s) + r.logger.InfoContext(ctx, "caught interrupt signal, exiting", slog.Any("signal", s)) case err := <-r.startCh: - r.logger.ErrorContext(ctx, "caught service error, exiting", "error", err) + r.logger.ErrorContext(ctx, "caught service error, exiting", errors.Attr(err)) return err } @@ -75,7 +75,7 @@ func (r *Registry) Wait(ctx context.Context) error { func (r *Registry) Stop(ctx context.Context) error { for _, s := range r.services.GetInOrder() { go func(s NamedService) { - r.logger.InfoContext(ctx, "stopping service", "service", s.Name()) + r.logger.InfoContext(ctx, "stopping service", slog.Any("service", s.Name())) err := s.Stop(ctx) r.stopCh <- err }(s) diff --git a/pkg/factory/settings.go b/pkg/factory/settings.go index 99ac2fc6196..7a93e958b4e 100644 --- a/pkg/factory/settings.go +++ b/pkg/factory/settings.go @@ -35,7 +35,7 @@ type scoped struct { func NewScopedProviderSettings(settings ProviderSettings, pkgName string) *scoped { return &scoped{ - logger: settings.Logger.With("logger", pkgName), + logger: settings.Logger.With(slog.String("logger", pkgName)), meter: settings.MeterProvider.Meter(pkgName), tracer: settings.TracerProvider.Tracer(pkgName), prometheusRegisterer: settings.PrometheusRegisterer, diff --git a/pkg/flagger/configflagger/configflagger.go b/pkg/flagger/configflagger/configflagger.go index b430fbea577..f094235eccd 100644 --- a/pkg/flagger/configflagger/configflagger.go +++ b/pkg/flagger/configflagger/configflagger.go @@ -2,6 +2,7 @@ package configflagger import ( "context" + "log/slog" "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/factory" @@ -34,7 +35,7 @@ func New(ctx context.Context, ps factory.ProviderSettings, c flagger.Config, reg feature, _, err := registry.GetByString(name) if err != nil { if errors.Ast(err, errors.TypeNotFound) { - settings.Logger().WarnContext(ctx, "skipping unknown feature flag", "name", name, "kind", "boolean") + settings.Logger().WarnContext(ctx, "skipping unknown feature flag", slog.String("name", name), slog.String("kind", "boolean")) continue } return nil, err @@ -52,7 +53,7 @@ func New(ctx context.Context, ps factory.ProviderSettings, c flagger.Config, reg feature, _, err := registry.GetByString(name) if err != nil { if errors.Ast(err, errors.TypeNotFound) { - settings.Logger().WarnContext(ctx, "skipping unknown feature flag", "name", name, "kind", "string") + settings.Logger().WarnContext(ctx, "skipping unknown feature flag", slog.String("name", name), slog.String("kind", "string")) continue } return nil, err @@ -70,7 +71,7 @@ func New(ctx context.Context, ps factory.ProviderSettings, c flagger.Config, reg feature, _, err := registry.GetByString(name) if err != nil { if errors.Ast(err, errors.TypeNotFound) { - settings.Logger().WarnContext(ctx, "skipping unknown feature flag", "name", name, "kind", "float") + settings.Logger().WarnContext(ctx, "skipping unknown feature flag", slog.String("name", name), slog.String("kind", "float")) continue } return nil, err @@ -88,7 +89,7 @@ func New(ctx context.Context, ps factory.ProviderSettings, c flagger.Config, reg feature, _, err := registry.GetByString(name) if err != nil { if errors.Ast(err, errors.TypeNotFound) { - settings.Logger().WarnContext(ctx, "skipping unknown feature flag", "name", name, "kind", "integer") + settings.Logger().WarnContext(ctx, "skipping unknown feature flag", slog.String("name", name), slog.String("kind", "integer")) continue } return nil, err @@ -106,7 +107,7 @@ func New(ctx context.Context, ps factory.ProviderSettings, c flagger.Config, reg feature, _, err := registry.GetByString(name) if err != nil { if errors.Ast(err, errors.TypeNotFound) { - settings.Logger().WarnContext(ctx, "skipping unknown feature flag", "name", name, "kind", "object") + settings.Logger().WarnContext(ctx, "skipping unknown feature flag", slog.String("name", name), slog.String("kind", "object")) continue } return nil, err diff --git a/pkg/flagger/flagger.go b/pkg/flagger/flagger.go index 16d8906bf3f..9b782035433 100644 --- a/pkg/flagger/flagger.go +++ b/pkg/flagger/flagger.go @@ -2,10 +2,13 @@ package flagger import ( "context" + "log/slog" + "github.com/open-feature/go-sdk/openfeature" + + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/types/featuretypes" - "github.com/open-feature/go-sdk/openfeature" ) // Any feature flag provider has to implement this interface. @@ -95,7 +98,7 @@ func (f *flagger) Boolean(ctx context.Context, flag featuretypes.Name, evalCtx f // check if the feature is present in the default registry feature, _, err := f.registry.Get(flag) if err != nil { - f.settings.Logger().ErrorContext(ctx, "failed to get feature from default registry", "error", err, "flag", flag) + f.settings.Logger().ErrorContext(ctx, "failed to get feature from default registry", errors.Attr(err), slog.Any("flag", flag)) return false, err } @@ -103,7 +106,7 @@ func (f *flagger) Boolean(ctx context.Context, flag featuretypes.Name, evalCtx f defaultValue, _, err := featuretypes.VariantValue[bool](feature, feature.DefaultVariant) if err != nil { // something which should never happen - f.settings.Logger().ErrorContext(ctx, "failed to get default value from feature", "error", err, "flag", flag) + f.settings.Logger().ErrorContext(ctx, "failed to get default value from feature", errors.Attr(err), slog.Any("flag", flag)) return false, err } @@ -112,7 +115,7 @@ func (f *flagger) Boolean(ctx context.Context, flag featuretypes.Name, evalCtx f for _, client := range f.clients { value, err := client.BooleanValue(ctx, flag.String(), defaultValue, evalCtx.Ctx()) if err != nil { - f.settings.Logger().ErrorContext(ctx, "failed to get value from client", "error", err, "flag", flag, "client", client.Metadata().Name) + f.settings.Logger().ErrorContext(ctx, "failed to get value from client", errors.Attr(err), slog.Any("flag", flag), slog.String("client", client.Metadata().Name())) continue } @@ -128,7 +131,7 @@ func (f *flagger) String(ctx context.Context, flag featuretypes.Name, evalCtx fe // check if the feature is present in the default registry feature, _, err := f.registry.Get(flag) if err != nil { - f.settings.Logger().ErrorContext(ctx, "failed to get feature from default registry", "error", err, "flag", flag) + f.settings.Logger().ErrorContext(ctx, "failed to get feature from default registry", errors.Attr(err), slog.Any("flag", flag)) return "", err } @@ -136,7 +139,7 @@ func (f *flagger) String(ctx context.Context, flag featuretypes.Name, evalCtx fe defaultValue, _, err := featuretypes.VariantValue[string](feature, feature.DefaultVariant) if err != nil { // something which should never happen - f.settings.Logger().ErrorContext(ctx, "failed to get default value from feature", "error", err, "flag", flag) + f.settings.Logger().ErrorContext(ctx, "failed to get default value from feature", errors.Attr(err), slog.Any("flag", flag)) return "", err } @@ -145,7 +148,7 @@ func (f *flagger) String(ctx context.Context, flag featuretypes.Name, evalCtx fe for _, client := range f.clients { value, err := client.StringValue(ctx, flag.String(), defaultValue, evalCtx.Ctx()) if err != nil { - f.settings.Logger().WarnContext(ctx, "failed to get value from client", "error", err, "flag", flag, "client", client.Metadata().Name) + f.settings.Logger().WarnContext(ctx, "failed to get value from client", errors.Attr(err), slog.Any("flag", flag), slog.String("client", client.Metadata().Name())) continue } @@ -161,7 +164,7 @@ func (f *flagger) Float(ctx context.Context, flag featuretypes.Name, evalCtx fea // check if the feature is present in the default registry feature, _, err := f.registry.Get(flag) if err != nil { - f.settings.Logger().ErrorContext(ctx, "failed to get feature from default registry", "error", err, "flag", flag) + f.settings.Logger().ErrorContext(ctx, "failed to get feature from default registry", errors.Attr(err), slog.Any("flag", flag)) return 0, err } @@ -169,7 +172,7 @@ func (f *flagger) Float(ctx context.Context, flag featuretypes.Name, evalCtx fea defaultValue, _, err := featuretypes.VariantValue[float64](feature, feature.DefaultVariant) if err != nil { // something which should never happen - f.settings.Logger().ErrorContext(ctx, "failed to get default value from feature", "error", err, "flag", flag) + f.settings.Logger().ErrorContext(ctx, "failed to get default value from feature", errors.Attr(err), slog.Any("flag", flag)) return 0, err } @@ -178,7 +181,7 @@ func (f *flagger) Float(ctx context.Context, flag featuretypes.Name, evalCtx fea for _, client := range f.clients { value, err := client.FloatValue(ctx, flag.String(), defaultValue, evalCtx.Ctx()) if err != nil { - f.settings.Logger().WarnContext(ctx, "failed to get value from client", "error", err, "flag", flag, "client", client.Metadata().Name) + f.settings.Logger().WarnContext(ctx, "failed to get value from client", errors.Attr(err), slog.Any("flag", flag), slog.String("client", client.Metadata().Name())) continue } @@ -194,7 +197,7 @@ func (f *flagger) Int(ctx context.Context, flag featuretypes.Name, evalCtx featu // check if the feature is present in the default registry feature, _, err := f.registry.Get(flag) if err != nil { - f.settings.Logger().ErrorContext(ctx, "failed to get feature from default registry", "error", err, "flag", flag) + f.settings.Logger().ErrorContext(ctx, "failed to get feature from default registry", errors.Attr(err), slog.Any("flag", flag)) return 0, err } @@ -202,7 +205,7 @@ func (f *flagger) Int(ctx context.Context, flag featuretypes.Name, evalCtx featu defaultValue, _, err := featuretypes.VariantValue[int64](feature, feature.DefaultVariant) if err != nil { // something which should never happen - f.settings.Logger().ErrorContext(ctx, "failed to get default value from feature", "error", err, "flag", flag) + f.settings.Logger().ErrorContext(ctx, "failed to get default value from feature", errors.Attr(err), slog.Any("flag", flag)) return 0, err } @@ -211,7 +214,7 @@ func (f *flagger) Int(ctx context.Context, flag featuretypes.Name, evalCtx featu for _, client := range f.clients { value, err := client.IntValue(ctx, flag.String(), defaultValue, evalCtx.Ctx()) if err != nil { - f.settings.Logger().WarnContext(ctx, "failed to get value from client", "error", err, "flag", flag, "client", client.Metadata().Name) + f.settings.Logger().WarnContext(ctx, "failed to get value from client", errors.Attr(err), slog.Any("flag", flag), slog.String("client", client.Metadata().Name())) continue } @@ -227,7 +230,7 @@ func (f *flagger) Object(ctx context.Context, flag featuretypes.Name, evalCtx fe // check if the feature is present in the default registry feature, _, err := f.registry.Get(flag) if err != nil { - f.settings.Logger().ErrorContext(ctx, "failed to get feature from default registry", "error", err, "flag", flag) + f.settings.Logger().ErrorContext(ctx, "failed to get feature from default registry", errors.Attr(err), slog.Any("flag", flag)) return nil, err } @@ -235,7 +238,7 @@ func (f *flagger) Object(ctx context.Context, flag featuretypes.Name, evalCtx fe defaultValue, _, err := featuretypes.VariantValue[any](feature, feature.DefaultVariant) if err != nil { // something which should never happen - f.settings.Logger().ErrorContext(ctx, "failed to get default value from feature", "error", err, "flag", flag) + f.settings.Logger().ErrorContext(ctx, "failed to get default value from feature", errors.Attr(err), slog.Any("flag", flag)) return nil, err } @@ -244,7 +247,7 @@ func (f *flagger) Object(ctx context.Context, flag featuretypes.Name, evalCtx fe for _, client := range f.clients { value, err := client.ObjectValue(ctx, flag.String(), defaultValue, evalCtx.Ctx()) if err != nil { - f.settings.Logger().WarnContext(ctx, "failed to get value from client", "error", err, "flag", flag, "client", client.Metadata().Name) + f.settings.Logger().WarnContext(ctx, "failed to get value from client", errors.Attr(err), slog.Any("flag", flag), slog.String("client", client.Metadata().Name())) continue } @@ -262,7 +265,7 @@ func (f *flagger) BooleanOrEmpty(ctx context.Context, flag featuretypes.Name, ev defaultValue := false value, err := f.Boolean(ctx, flag, evalCtx) if err != nil { - f.settings.Logger().ErrorContext(ctx, "failed to get value from flagger service", "error", err, "flag", flag) + f.settings.Logger().ErrorContext(ctx, "failed to get value from flagger service", errors.Attr(err), slog.Any("flag", flag)) return defaultValue } return value @@ -272,7 +275,7 @@ func (f *flagger) StringOrEmpty(ctx context.Context, flag featuretypes.Name, eva defaultValue := "" value, err := f.String(ctx, flag, evalCtx) if err != nil { - f.settings.Logger().ErrorContext(ctx, "failed to get value from flagger service", "error", err, "flag", flag) + f.settings.Logger().ErrorContext(ctx, "failed to get value from flagger service", errors.Attr(err), slog.Any("flag", flag)) return defaultValue } return value @@ -282,7 +285,7 @@ func (f *flagger) FloatOrEmpty(ctx context.Context, flag featuretypes.Name, eval defaultValue := 0.0 value, err := f.Float(ctx, flag, evalCtx) if err != nil { - f.settings.Logger().ErrorContext(ctx, "failed to get value from flagger service", "error", err, "flag", flag) + f.settings.Logger().ErrorContext(ctx, "failed to get value from flagger service", errors.Attr(err), slog.Any("flag", flag)) return defaultValue } return value @@ -292,7 +295,7 @@ func (f *flagger) IntOrEmpty(ctx context.Context, flag featuretypes.Name, evalCt defaultValue := int64(0) value, err := f.Int(ctx, flag, evalCtx) if err != nil { - f.settings.Logger().ErrorContext(ctx, "failed to get value from flagger service", "error", err, "flag", flag) + f.settings.Logger().ErrorContext(ctx, "failed to get value from flagger service", errors.Attr(err), slog.Any("flag", flag)) return defaultValue } return value @@ -302,7 +305,7 @@ func (f *flagger) ObjectOrEmpty(ctx context.Context, flag featuretypes.Name, eva defaultValue := struct{}{} value, err := f.Object(ctx, flag, evalCtx) if err != nil { - f.settings.Logger().ErrorContext(ctx, "failed to get value from flagger service", "error", err, "flag", flag) + f.settings.Logger().ErrorContext(ctx, "failed to get value from flagger service", errors.Attr(err), slog.Any("flag", flag)) return defaultValue } return value @@ -336,7 +339,7 @@ func (f *flagger) List(ctx context.Context, evalCtx featuretypes.FlaggerEvaluati for _, provider := range f.providers { pFeatures, err := provider.List(ctx) if err != nil { - f.settings.Logger().WarnContext(ctx, "failed to get features from provider", "error", err, "provider", provider.Metadata().Name) + f.settings.Logger().WarnContext(ctx, "failed to get features from provider", errors.Attr(err), slog.String("provider", provider.Metadata().Name)) continue } diff --git a/pkg/http/client/plugin/log.go b/pkg/http/client/plugin/log.go index 9bd60e951be..b2fb3374038 100644 --- a/pkg/http/client/plugin/log.go +++ b/pkg/http/client/plugin/log.go @@ -9,6 +9,8 @@ import ( "github.com/gojek/heimdall/v7" semconv "go.opentelemetry.io/otel/semconv/v1.27.0" + + "github.com/SigNoz/signoz/pkg/errors" ) type reqResLog struct { @@ -51,7 +53,7 @@ func (plugin *reqResLog) OnRequestEnd(request *http.Request, response *http.Resp bodybytes, err := io.ReadAll(response.Body) if err != nil { - plugin.logger.DebugContext(request.Context(), "::UNABLE-TO-LOG-RESPONSE-BODY::", "error", err) + plugin.logger.DebugContext(request.Context(), "::UNABLE-TO-LOG-RESPONSE-BODY::", errors.Attr(err)) } else { _ = response.Body.Close() response.Body = io.NopCloser(bytes.NewBuffer(bodybytes)) @@ -69,7 +71,7 @@ func (plugin *reqResLog) OnRequestEnd(request *http.Request, response *http.Resp func (plugin *reqResLog) OnError(request *http.Request, err error) { host, port, _ := net.SplitHostPort(request.Host) fields := []any{ - "error", err, + errors.Attr(err), string(semconv.HTTPRequestMethodKey), request.Method, string(semconv.URLPathKey), request.URL.Path, string(semconv.URLSchemeKey), request.URL.Scheme, diff --git a/pkg/http/middleware/authz.go b/pkg/http/middleware/authz.go index d6afc1ba53f..9ec9bfaf8db 100644 --- a/pkg/http/middleware/authz.go +++ b/pkg/http/middleware/authz.go @@ -42,7 +42,7 @@ func (middleware *AuthZ) ViewAccess(next http.HandlerFunc) http.HandlerFunc { if claims.IdentNProvider == authtypes.IdentNProviderAPIKey.StringValue() { if err := claims.IsViewer(); err != nil { - middleware.logger.WarnContext(ctx, authzDeniedMessage, "claims", claims) + middleware.logger.WarnContext(ctx, authzDeniedMessage, slog.Any("claims", claims)) render.Error(rw, err) return } @@ -67,7 +67,7 @@ func (middleware *AuthZ) ViewAccess(next http.HandlerFunc) http.HandlerFunc { selectors, ) if err != nil { - middleware.logger.WarnContext(ctx, authzDeniedMessage, "claims", claims) + middleware.logger.WarnContext(ctx, authzDeniedMessage, slog.Any("claims", claims)) if errors.Asc(err, authtypes.ErrCodeAuthZForbidden) { render.Error(rw, errors.New(errors.TypeForbidden, authtypes.ErrCodeAuthZForbidden, "only viewers/editors/admins can access this resource")) return @@ -92,7 +92,7 @@ func (middleware *AuthZ) EditAccess(next http.HandlerFunc) http.HandlerFunc { if claims.IdentNProvider == authtypes.IdentNProviderAPIKey.StringValue() { if err := claims.IsEditor(); err != nil { - middleware.logger.WarnContext(ctx, authzDeniedMessage, "claims", claims) + middleware.logger.WarnContext(ctx, authzDeniedMessage, slog.Any("claims", claims)) render.Error(rw, err) return } @@ -116,7 +116,7 @@ func (middleware *AuthZ) EditAccess(next http.HandlerFunc) http.HandlerFunc { selectors, ) if err != nil { - middleware.logger.WarnContext(ctx, authzDeniedMessage, "claims", claims) + middleware.logger.WarnContext(ctx, authzDeniedMessage, slog.Any("claims", claims)) if errors.Asc(err, authtypes.ErrCodeAuthZForbidden) { render.Error(rw, errors.New(errors.TypeForbidden, authtypes.ErrCodeAuthZForbidden, "only editors/admins can access this resource")) return @@ -141,7 +141,7 @@ func (middleware *AuthZ) AdminAccess(next http.HandlerFunc) http.HandlerFunc { if claims.IdentNProvider == authtypes.IdentNProviderAPIKey.StringValue() { if err := claims.IsAdmin(); err != nil { - middleware.logger.WarnContext(ctx, authzDeniedMessage, "claims", claims) + middleware.logger.WarnContext(ctx, authzDeniedMessage, slog.Any("claims", claims)) render.Error(rw, err) return } @@ -164,7 +164,7 @@ func (middleware *AuthZ) AdminAccess(next http.HandlerFunc) http.HandlerFunc { selectors, ) if err != nil { - middleware.logger.WarnContext(ctx, authzDeniedMessage, "claims", claims) + middleware.logger.WarnContext(ctx, authzDeniedMessage, slog.Any("claims", claims)) if errors.Asc(err, authtypes.ErrCodeAuthZForbidden) { render.Error(rw, errors.New(errors.TypeForbidden, authtypes.ErrCodeAuthZForbidden, "only admins can access this resource")) return @@ -188,7 +188,7 @@ func (middleware *AuthZ) SelfAccess(next http.HandlerFunc) http.HandlerFunc { id := mux.Vars(req)["id"] if err := claims.IsSelfAccess(id); err != nil { - middleware.logger.WarnContext(req.Context(), authzDeniedMessage, "claims", claims) + middleware.logger.WarnContext(req.Context(), authzDeniedMessage, slog.Any("claims", claims)) render.Error(rw, err) return } diff --git a/pkg/http/middleware/identn.go b/pkg/http/middleware/identn.go index ccf89a10864..2f4474d4875 100644 --- a/pkg/http/middleware/identn.go +++ b/pkg/http/middleware/identn.go @@ -5,6 +5,7 @@ import ( "log/slog" "net/http" + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/identn" "github.com/SigNoz/signoz/pkg/sharder" "github.com/SigNoz/signoz/pkg/types" @@ -52,7 +53,7 @@ func (m *IdentN) Wrap(next http.Handler) http.Handler { ctx := r.Context() claims := identity.ToClaims() if err := m.sharder.IsMyOwnedKey(ctx, types.NewOrganizationKey(valuer.MustNewUUID(claims.OrgID))); err != nil { - m.logger.ErrorContext(ctx, identityCrossOrgMessage, "claims", claims, "error", err) + m.logger.ErrorContext(ctx, identityCrossOrgMessage, slog.Any("claims", claims), errors.Attr(err)) next.ServeHTTP(w, r) return } diff --git a/pkg/http/middleware/logging.go b/pkg/http/middleware/logging.go index 8a967a5c729..7c03165c221 100644 --- a/pkg/http/middleware/logging.go +++ b/pkg/http/middleware/logging.go @@ -9,6 +9,8 @@ import ( "github.com/gorilla/mux" semconv "go.opentelemetry.io/otel/semconv/v1.26.0" + + "github.com/SigNoz/signoz/pkg/errors" ) const ( @@ -27,7 +29,7 @@ func NewLogging(logger *slog.Logger, excludedRoutes []string) *Logging { } return &Logging{ - logger: logger.With("pkg", pkgname), + logger: logger.With(slog.String("pkg", pkgname)), excludedRoutes: excludedRoutesMap, } } @@ -65,7 +67,7 @@ func (middleware *Logging) Wrap(next http.Handler) http.Handler { string(semconv.HTTPServerRequestDurationName), time.Since(start), ) if err != nil { - fields = append(fields, "error", err) + fields = append(fields, errors.Attr(err)) middleware.logger.ErrorContext(req.Context(), logMessage, fields...) } else { // when the status code is 400 or >=500, and the response body is not empty. diff --git a/pkg/http/middleware/timeout.go b/pkg/http/middleware/timeout.go index 9909336be78..1e29d920d69 100644 --- a/pkg/http/middleware/timeout.go +++ b/pkg/http/middleware/timeout.go @@ -6,6 +6,8 @@ import ( "net/http" "strings" "time" + + "github.com/SigNoz/signoz/pkg/errors" ) const ( @@ -36,7 +38,7 @@ func NewTimeout(logger *slog.Logger, excludedRoutes []string, defaultTimeout tim } return &Timeout{ - logger: logger.With("pkg", pkgname), + logger: logger.With(slog.String("pkg", pkgname)), excluded: excluded, defaultTimeout: defaultTimeout, maxTimeout: maxTimeout, @@ -51,7 +53,7 @@ func (middleware *Timeout) Wrap(next http.Handler) http.Handler { if incoming != "" { parsed, err := time.ParseDuration(strings.TrimSpace(incoming) + "s") if err != nil { - middleware.logger.WarnContext(req.Context(), "cannot parse timeout in header, using default timeout", "timeout", incoming, "error", err) + middleware.logger.WarnContext(req.Context(), "cannot parse timeout in header, using default timeout", slog.String("timeout", incoming), errors.Attr(err)) } else { if parsed > middleware.maxTimeout { actual = middleware.maxTimeout diff --git a/pkg/http/server/server.go b/pkg/http/server/server.go index 0c9d19bc871..a322f442ba7 100644 --- a/pkg/http/server/server.go +++ b/pkg/http/server/server.go @@ -38,17 +38,17 @@ func New(logger *slog.Logger, cfg Config, handler http.Handler) (*Server, error) return &Server{ srv: srv, - logger: logger.With("pkg", "go.signoz.io/pkg/http/server"), + logger: logger.With(slog.String("pkg", "go.signoz.io/pkg/http/server")), handler: handler, cfg: cfg, }, nil } func (server *Server) Start(ctx context.Context) error { - server.logger.InfoContext(ctx, "starting http server", "address", server.srv.Addr) + server.logger.InfoContext(ctx, "starting http server", slog.String("address", server.srv.Addr)) if err := server.srv.ListenAndServe(); err != nil { if err != http.ErrServerClosed { - server.logger.ErrorContext(ctx, "failed to start server", "error", err) + server.logger.ErrorContext(ctx, "failed to start server", errors.Attr(err)) return err } } @@ -60,7 +60,7 @@ func (server *Server) Stop(ctx context.Context) error { defer cancel() if err := server.srv.Shutdown(ctx); err != nil { - server.logger.ErrorContext(ctx, "failed to stop server", "error", err) + server.logger.ErrorContext(ctx, "failed to stop server", errors.Attr(err)) return err } diff --git a/pkg/identn/apikeyidentn/provider.go b/pkg/identn/apikeyidentn/provider.go index 28642e2a4ce..ba584a6d2a8 100644 --- a/pkg/identn/apikeyidentn/provider.go +++ b/pkg/identn/apikeyidentn/provider.go @@ -5,13 +5,14 @@ import ( "net/http" "time" + "golang.org/x/sync/singleflight" + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/identn" "github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types/authtypes" - "golang.org/x/sync/singleflight" ) // todo: will move this in types layer with service account integration @@ -118,7 +119,7 @@ func (provider *provider) Post(ctx context.Context, _ *http.Request, _ authtypes Where("revoked = false"). Exec(ctx) if err != nil { - provider.settings.Logger().ErrorContext(ctx, "failed to update last used of api key", "error", err) + provider.settings.Logger().ErrorContext(ctx, "failed to update last used of api key", errors.Attr(err)) } return true, nil }) diff --git a/pkg/identn/resolver.go b/pkg/identn/resolver.go index 2e22e38c7a7..2d9939d3500 100644 --- a/pkg/identn/resolver.go +++ b/pkg/identn/resolver.go @@ -2,6 +2,7 @@ package identn import ( "context" + "log/slog" "net/http" "github.com/SigNoz/signoz/pkg/factory" @@ -69,7 +70,7 @@ func NewIdentNResolver(ctx context.Context, providerSettings factory.ProviderSet func (c *identNResolver) GetIdentN(r *http.Request) IdentN { for _, idn := range c.identNs { if idn.Test(r) { - c.settings.Logger().DebugContext(r.Context(), "identN matched", "provider", idn.Name()) + c.settings.Logger().DebugContext(r.Context(), "identN matched", slog.Any("provider", idn.Name())) return idn } } diff --git a/pkg/identn/tokenizeridentn/provider.go b/pkg/identn/tokenizeridentn/provider.go index d17cd6bda99..cc7cee169ec 100644 --- a/pkg/identn/tokenizeridentn/provider.go +++ b/pkg/identn/tokenizeridentn/provider.go @@ -6,11 +6,13 @@ import ( "strings" "time" + "golang.org/x/sync/singleflight" + + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/identn" "github.com/SigNoz/signoz/pkg/tokenizer" "github.com/SigNoz/signoz/pkg/types/authtypes" - "golang.org/x/sync/singleflight" ) type provider struct { @@ -81,7 +83,7 @@ func (provider *provider) Post(ctx context.Context, _ *http.Request, _ authtypes _, _, _ = provider.sfGroup.Do(accessToken, func() (any, error) { if err := provider.tokenizer.SetLastObservedAt(ctx, accessToken, time.Now()); err != nil { - provider.settings.Logger().ErrorContext(ctx, "failed to set last observed at", "error", err) + provider.settings.Logger().ErrorContext(ctx, "failed to set last observed at", errors.Attr(err)) return false, err } return true, nil diff --git a/pkg/instrumentation/logger.go b/pkg/instrumentation/logger.go index 8eb02e7ae45..5663000a362 100644 --- a/pkg/instrumentation/logger.go +++ b/pkg/instrumentation/logger.go @@ -10,7 +10,7 @@ import ( type zapToSlogConverter struct{} -func NewLogger(config Config, wrappers ...loghandler.Wrapper) *slog.Logger { +func NewLogger(config Config) *slog.Logger { logger := slog.New( loghandler.New( slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: config.Logs.Level, AddSource: true, ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { @@ -27,7 +27,9 @@ func NewLogger(config Config, wrappers ...loghandler.Wrapper) *slog.Logger { return a }}), - wrappers..., + loghandler.NewCorrelation(), + loghandler.NewFiltering(), + loghandler.NewException(), ), ) diff --git a/pkg/instrumentation/loghandler/exception.go b/pkg/instrumentation/loghandler/exception.go new file mode 100644 index 00000000000..161ad540b95 --- /dev/null +++ b/pkg/instrumentation/loghandler/exception.go @@ -0,0 +1,53 @@ +package loghandler + +import ( + "context" + "log/slog" + + "github.com/SigNoz/signoz/pkg/errors" +) + +type exception struct{} + +func NewException() *exception { + return &exception{} +} + +func (h *exception) Wrap(next LogHandler) LogHandler { + return LogHandlerFunc(func(ctx context.Context, record slog.Record) error { + var foundErr error + newRecord := slog.NewRecord(record.Time, record.Level, record.Message, record.PC) + record.Attrs(func(a slog.Attr) bool { + if a.Key == "exception" { + if err, ok := a.Value.Any().(error); ok { + foundErr = err + return true + } + } + newRecord.AddAttrs(a) + return true + }) + + if foundErr == nil { + return next.Handle(ctx, record) + } + + t, c, m, _, _, _ := errors.Unwrapb(foundErr) + + newRecord.AddAttrs( + slog.String("exception.type", t.String()), + slog.String("exception.code", c.String()), + slog.String("exception.message", m), + ) + + // Use the stacktrace captured at error creation time if available. + type stacktracer interface { + Stacktrace() string + } + if st, ok := foundErr.(stacktracer); ok && st.Stacktrace() != "" { + newRecord.AddAttrs(slog.String("exception.stacktrace", st.Stacktrace())) + } + + return next.Handle(ctx, newRecord) + }) +} diff --git a/pkg/instrumentation/loghandler/exception_test.go b/pkg/instrumentation/loghandler/exception_test.go new file mode 100644 index 00000000000..6b02aa0c8cc --- /dev/null +++ b/pkg/instrumentation/loghandler/exception_test.go @@ -0,0 +1,100 @@ +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 TestException(t *testing.T) { + testCases := []struct { + name string + attrs []slog.Attr + exceptionType string + exceptionCode string + exceptionMessage string + hasException bool + }{ + { + name: "PkgError", + attrs: []slog.Attr{ + errors.Attr(errors.New(errors.TypeNotFound, errors.MustNewCode("test_code"), "resource not found")), + }, + exceptionType: "not-found", + exceptionCode: "test_code", + exceptionMessage: "resource not found", + hasException: true, + }, + { + name: "StdlibError", + attrs: []slog.Attr{ + errors.Attr(errors.Newf(errors.TypeInternal, errors.MustNewCode("internal"), "something went wrong")), + }, + exceptionType: "internal", + exceptionCode: "internal", + exceptionMessage: "something went wrong", + hasException: true, + }, + { + name: "WrappedPkgError", + attrs: []slog.Attr{ + errors.Attr(errors.Wrapf(errors.New(errors.TypeNotFound, errors.MustNewCode("not_found"), "db connection failed"), errors.TypeInternal, errors.MustNewCode("db_error"), "failed to fetch user")), + }, + exceptionType: "internal", + exceptionCode: "db_error", + exceptionMessage: "failed to fetch user", + hasException: true, + }, + { + name: "NoError", + attrs: []slog.Attr{ + slog.String("key", "value"), + }, + hasException: false, + }, + { + name: "ExceptionKeyWithNonError", + attrs: []slog.Attr{ + slog.String("exception", "not an error type"), + }, + hasException: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + buf := bytes.NewBuffer(nil) + logger := slog.New(&handler{ + base: slog.NewJSONHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}), + wrappers: []Wrapper{NewException()}, + }) + + logger.LogAttrs(context.Background(), slog.LevelError, "operation failed", tc.attrs...) + + m := make(map[string]any) + err := json.Unmarshal(buf.Bytes(), &m) + require.NoError(t, err) + assert.Equal(t, "operation failed", m["msg"]) + + if tc.hasException { + assert.Equal(t, tc.exceptionType, m["exception.type"]) + assert.Equal(t, tc.exceptionCode, m["exception.code"]) + assert.Equal(t, tc.exceptionMessage, m["exception.message"]) + stacktrace, ok := m["exception.stacktrace"].(string) + require.True(t, ok) + assert.Contains(t, stacktrace, "exception_test.go:") + } else { + assert.Nil(t, m["exception.type"]) + assert.Nil(t, m["exception.code"]) + assert.Nil(t, m["exception.message"]) + assert.Nil(t, m["exception.stacktrace"]) + } + }) + } +} diff --git a/pkg/instrumentation/loghandler/filtering_test.go b/pkg/instrumentation/loghandler/filtering_test.go index d10a9c86bd0..59a972454e1 100644 --- a/pkg/instrumentation/loghandler/filtering_test.go +++ b/pkg/instrumentation/loghandler/filtering_test.go @@ -18,7 +18,7 @@ func TestFiltering_SuppressesContextCanceled(t *testing.T) { 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) + logger.ErrorContext(context.Background(), "operation failed", slog.Any("error", context.Canceled)) assert.Empty(t, buf.String(), "log with context.Canceled should be suppressed") } @@ -29,7 +29,7 @@ func TestFiltering_AllowsOtherErrors(t *testing.T) { 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")) + logger.ErrorContext(context.Background(), "operation failed", slog.Any("error", errors.New(errors.TypeInternal, errors.CodeInternal, "some other error"))) m := make(map[string]any) err := json.Unmarshal(buf.Bytes(), &m) @@ -43,7 +43,7 @@ func TestFiltering_AllowsLogsWithoutErrors(t *testing.T) { 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") + logger.InfoContext(context.Background(), "normal log", slog.String("key", "value")) m := make(map[string]any) err := json.Unmarshal(buf.Bytes(), &m) diff --git a/pkg/instrumentation/sdk.go b/pkg/instrumentation/sdk.go index 8d3bb3c0818..6de93d76e31 100644 --- a/pkg/instrumentation/sdk.go +++ b/pkg/instrumentation/sdk.go @@ -6,7 +6,6 @@ import ( "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/factory" - "github.com/SigNoz/signoz/pkg/instrumentation/loghandler" "github.com/SigNoz/signoz/pkg/version" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" @@ -116,7 +115,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), startCh: make(chan struct{}), }, nil } diff --git a/pkg/modules/dashboard/impldashboard/module.go b/pkg/modules/dashboard/impldashboard/module.go index b393570c302..e5a68a3a491 100644 --- a/pkg/modules/dashboard/impldashboard/module.go +++ b/pkg/modules/dashboard/impldashboard/module.go @@ -2,6 +2,7 @@ package impldashboard import ( "context" + "log/slog" "slices" "github.com/SigNoz/signoz/pkg/analytics" @@ -308,7 +309,7 @@ func (module *module) checkClickHouseQueriesForMetricNames(ctx context.Context, result, err := module.queryParser.AnalyzeQueryFilter(ctx, qbtypes.QueryTypeClickHouseSQL, queryStr) if err != nil { // Log warning and continue - parsing errors shouldn't break the search - module.settings.Logger().WarnContext(ctx, "failed to parse ClickHouse query", "query", queryStr, "error", err) + module.settings.Logger().WarnContext(ctx, "failed to parse ClickHouse query", slog.String("query", queryStr), errors.Attr(err)) continue } @@ -343,7 +344,7 @@ func (module *module) checkPromQLQueriesForMetricNames(ctx context.Context, quer result, err := module.queryParser.AnalyzeQueryFilter(ctx, qbtypes.QueryTypePromQL, queryStr) if err != nil { // Log warning and continue - parsing errors shouldn't break the search - module.settings.Logger().WarnContext(ctx, "failed to parse PromQL query", "query", queryStr, "error", err) + module.settings.Logger().WarnContext(ctx, "failed to parse PromQL query", slog.String("query", queryStr), errors.Attr(err)) continue } diff --git a/pkg/modules/metricsexplorer/implmetricsexplorer/module.go b/pkg/modules/metricsexplorer/implmetricsexplorer/module.go index 71bc6dab474..082865fa848 100644 --- a/pkg/modules/metricsexplorer/implmetricsexplorer/module.go +++ b/pkg/modules/metricsexplorer/implmetricsexplorer/module.go @@ -8,6 +8,9 @@ import ( "strings" "time" + sqlbuilder "github.com/huandu/go-sqlbuilder" + "golang.org/x/sync/errgroup" + "github.com/SigNoz/signoz/pkg/cache" "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/factory" @@ -25,8 +28,6 @@ import ( "github.com/SigNoz/signoz/pkg/types/ruletypes" "github.com/SigNoz/signoz/pkg/types/telemetrytypes" "github.com/SigNoz/signoz/pkg/valuer" - sqlbuilder "github.com/huandu/go-sqlbuilder" - "golang.org/x/sync/errgroup" ) type module struct { @@ -510,7 +511,7 @@ func (m *module) fetchMetadataFromCache(ctx context.Context, orgID valuer.UUID, if err := m.cache.Get(ctx, orgID, cacheKey, &cachedMetadata); err == nil { hits[metricName] = &cachedMetadata } else { - m.logger.WarnContext(ctx, "cache miss for metric metadata", "metric_name", metricName, "error", err) + m.logger.WarnContext(ctx, "cache miss for metric metadata", slog.String("metric_name", metricName), errors.Attr(err)) misses = append(misses, metricName) } } @@ -566,7 +567,7 @@ func (m *module) fetchUpdatedMetadata(ctx context.Context, orgID valuer.UUID, me cacheKey := generateMetricMetadataCacheKey(metricName) if err := m.cache.Set(ctx, orgID, cacheKey, &metricMetadata, 0); err != nil { - m.logger.WarnContext(ctx, "failed to set metric metadata in cache", "metric_name", metricName, "error", err) + m.logger.WarnContext(ctx, "failed to set metric metadata in cache", slog.String("metric_name", metricName), errors.Attr(err)) } } @@ -626,7 +627,7 @@ func (m *module) fetchTimeseriesMetadata(ctx context.Context, orgID valuer.UUID, cacheKey := generateMetricMetadataCacheKey(metricName) if err := m.cache.Set(ctx, orgID, cacheKey, &metricMetadata, 0); err != nil { - m.logger.WarnContext(ctx, "failed to set metric metadata in cache", "metric_name", metricName, "error", err) + m.logger.WarnContext(ctx, "failed to set metric metadata in cache", slog.String("metric_name", metricName), errors.Attr(err)) } } @@ -765,7 +766,7 @@ func (m *module) insertMetricsMetadata(ctx context.Context, orgID valuer.UUID, r } cacheKey := generateMetricMetadataCacheKey(req.MetricName) if err := m.cache.Set(ctx, orgID, cacheKey, metricMetadata, 0); err != nil { - m.logger.WarnContext(ctx, "failed to set metric metadata in cache after insert", "metric_name", req.MetricName, "error", err) + m.logger.WarnContext(ctx, "failed to set metric metadata in cache after insert", slog.String("metric_name", req.MetricName), errors.Attr(err)) } return nil diff --git a/pkg/modules/serviceaccount/implserviceaccount/module.go b/pkg/modules/serviceaccount/implserviceaccount/module.go index d07fdcde088..61e226216e5 100644 --- a/pkg/modules/serviceaccount/implserviceaccount/module.go +++ b/pkg/modules/serviceaccount/implserviceaccount/module.go @@ -273,7 +273,7 @@ func (module *module) CreateFactorAPIKey(ctx context.Context, factorAPIKey *serv "KeyID": factorAPIKey.ID.String(), "KeyCreatedAt": factorAPIKey.CreatedAt.String(), }); err != nil { - module.settings.Logger().ErrorContext(ctx, "failed to send email", "error", err) + module.settings.Logger().ErrorContext(ctx, "failed to send email", errors.Attr(err)) } return nil @@ -328,7 +328,7 @@ func (module *module) RevokeFactorAPIKey(ctx context.Context, serviceAccountID v "KeyID": factorAPIKey.ID.String(), "KeyCreatedAt": factorAPIKey.CreatedAt.String(), }); err != nil { - module.settings.Logger().ErrorContext(ctx, "failed to send email", "error", err) + module.settings.Logger().ErrorContext(ctx, "failed to send email", errors.Attr(err)) } return nil diff --git a/pkg/modules/session/implsession/module.go b/pkg/modules/session/implsession/module.go index 9dabcf7f004..d2e84a6f417 100644 --- a/pkg/modules/session/implsession/module.go +++ b/pkg/modules/session/implsession/module.go @@ -2,6 +2,7 @@ package implsession import ( "context" + "log/slog" "net/url" "slices" "strings" @@ -132,7 +133,7 @@ func (module *module) CreateCallbackAuthNSession(ctx context.Context, authNProvi callbackIdentity, err := callbackAuthN.HandleCallback(ctx, values) if err != nil { - module.settings.Logger().ErrorContext(ctx, "failed to handle callback", "error", err, "authn_provider", authNProvider) + module.settings.Logger().ErrorContext(ctx, "failed to handle callback", errors.Attr(err), slog.Any("authn_provider", authNProvider)) return "", err } diff --git a/pkg/modules/user/impluser/module.go b/pkg/modules/user/impluser/module.go index d4093106e09..a1aed885d1c 100644 --- a/pkg/modules/user/impluser/module.go +++ b/pkg/modules/user/impluser/module.go @@ -2,10 +2,13 @@ package impluser import ( "context" + "log/slog" "slices" "strings" "time" + "github.com/dustin/go-humanize" + "github.com/SigNoz/signoz/pkg/analytics" "github.com/SigNoz/signoz/pkg/authz" "github.com/SigNoz/signoz/pkg/emailing" @@ -20,7 +23,6 @@ import ( "github.com/SigNoz/signoz/pkg/types/emailtypes" "github.com/SigNoz/signoz/pkg/types/integrationtypes" "github.com/SigNoz/signoz/pkg/valuer" - "github.com/dustin/go-humanize" ) type Module struct { @@ -107,7 +109,7 @@ func (m *Module) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID // generate reset password token resetPasswordToken, err := m.GetOrCreateResetPasswordToken(ctx, newUser.ID) if err != nil { - m.settings.Logger().ErrorContext(ctx, "failed to create reset password token for invited user", "error", err) + m.settings.Logger().ErrorContext(ctx, "failed to create reset password token for invited user", errors.Attr(err)) return err } @@ -149,7 +151,7 @@ func (m *Module) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID frontendBaseUrl := bulkInvites.Invites[idx].FrontendBaseUrl if frontendBaseUrl == "" { - m.settings.Logger().InfoContext(ctx, "frontend base url is not provided, skipping email", "invitee_email", userWithToken.User.Email) + m.settings.Logger().InfoContext(ctx, "frontend base url is not provided, skipping email", slog.Any("invitee_email", userWithToken.User.Email)) continue } @@ -163,7 +165,7 @@ func (m *Module) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID "link": resetLink, "Expiry": humanizedTokenLifetime, }); err != nil { - m.settings.Logger().ErrorContext(ctx, "failed to send invite email", "error", err) + m.settings.Logger().ErrorContext(ctx, "failed to send invite email", errors.Attr(err)) } } @@ -405,7 +407,7 @@ func (module *Module) ForgotPassword(ctx context.Context, orgID valuer.UUID, ema token, err := module.GetOrCreateResetPasswordToken(ctx, user.ID) if err != nil { - module.settings.Logger().ErrorContext(ctx, "failed to create reset password token", "error", err) + module.settings.Logger().ErrorContext(ctx, "failed to create reset password token", errors.Attr(err)) return err } @@ -427,7 +429,7 @@ func (module *Module) ForgotPassword(ctx context.Context, orgID valuer.UUID, ema "Expiry": humanizedTokenLifetime, }, ); err != nil { - module.settings.Logger().ErrorContext(ctx, "failed to send reset password email", "error", err) + module.settings.Logger().ErrorContext(ctx, "failed to send reset password email", errors.Attr(err)) return nil } diff --git a/pkg/modules/user/impluser/service.go b/pkg/modules/user/impluser/service.go index f48b66a6917..60a50eb8ba1 100644 --- a/pkg/modules/user/impluser/service.go +++ b/pkg/modules/user/impluser/service.go @@ -60,7 +60,7 @@ func (s *service) Start(ctx context.Context) error { return nil } - s.settings.Logger().WarnContext(ctx, "root user reconciliation failed, retrying", "error", err) + s.settings.Logger().WarnContext(ctx, "root user reconciliation failed, retrying", errors.Attr(err)) select { case <-s.stopC: diff --git a/pkg/querier/api.go b/pkg/querier/api.go index 7aab3267562..e7cf902a908 100644 --- a/pkg/querier/api.go +++ b/pkg/querier/api.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "log/slog" "net/http" "runtime/debug" "strconv" @@ -58,10 +59,10 @@ func (handler *handler) QueryRange(rw http.ResponseWriter, req *http.Request) { queryJSON, _ := json.Marshal(queryRangeRequest) handler.set.Logger.ErrorContext(ctx, "panic in QueryRange", - "error", r, - "user", claims.UserID, - "payload", string(queryJSON), - "stacktrace", stackTrace, + slog.Any("error", r), + slog.Any("user", claims.UserID), + slog.String("payload", string(queryJSON)), + slog.String("stacktrace", stackTrace), ) render.Error(rw, errors.NewInternalf( @@ -158,10 +159,10 @@ func (handler *handler) QueryRawStream(rw http.ResponseWriter, req *http.Request queryJSON, _ := json.Marshal(queryRangeRequest) handler.set.Logger.ErrorContext(ctx, "panic in QueryRawStream", - "error", r, - "user", claims.UserID, - "payload", string(queryJSON), - "stacktrace", stackTrace, + slog.Any("error", r), + slog.Any("user", claims.UserID), + slog.String("payload", string(queryJSON)), + slog.String("stacktrace", stackTrace), ) render.Error(rw, errors.NewInternalf( diff --git a/pkg/querier/bucket_cache.go b/pkg/querier/bucket_cache.go index 8a272563e2c..cc20cf2b1cb 100644 --- a/pkg/querier/bucket_cache.go +++ b/pkg/querier/bucket_cache.go @@ -47,19 +47,19 @@ func (bc *bucketCache) GetMissRanges( // Get query window startMs, endMs := q.Window() - bc.logger.DebugContext(ctx, "getting miss ranges", "fingerprint", q.Fingerprint(), "start", startMs, "end", endMs) + bc.logger.DebugContext(ctx, "getting miss ranges", slog.String("fingerprint", q.Fingerprint()), slog.Uint64("start", startMs), slog.Uint64("end", endMs)) // Generate cache key cacheKey := bc.generateCacheKey(q) - bc.logger.DebugContext(ctx, "cache key", "cache_key", cacheKey) + bc.logger.DebugContext(ctx, "cache key", slog.String("cache_key", cacheKey)) // Try to get cached data var data qbtypes.CachedData err := bc.cache.Get(ctx, orgID, cacheKey, &data) if err != nil { if !errors.Ast(err, errors.TypeNotFound) { - bc.logger.ErrorContext(ctx, "error getting cached data", "error", err) + bc.logger.ErrorContext(ctx, "error getting cached data", errors.Attr(err)) } // No cached data, need to fetch entire range missing = []*qbtypes.TimeRange{{From: startMs, To: endMs}} @@ -71,7 +71,7 @@ func (bc *bucketCache) GetMissRanges( // Find missing ranges with step alignment missing = bc.findMissingRangesWithStep(data.Buckets, startMs, endMs, stepMs) - bc.logger.DebugContext(ctx, "missing ranges", "missing", missing, "step", stepMs) + bc.logger.DebugContext(ctx, "missing ranges", slog.Any("missing", missing), slog.Uint64("step", stepMs)) // If no cached data overlaps with requested range, return empty result if len(data.Buckets) == 0 { @@ -105,9 +105,9 @@ func (bc *bucketCache) Put(ctx context.Context, orgID valuer.UUID, q qbtypes.Que // If the entire range is within flux interval, skip caching if startMs >= fluxBoundary { bc.logger.DebugContext(ctx, "entire range within flux interval, skipping cache", - "start", startMs, - "end", endMs, - "flux_boundary", fluxBoundary) + slog.Uint64("start", startMs), + slog.Uint64("end", endMs), + slog.Uint64("flux_boundary", fluxBoundary)) return } @@ -116,8 +116,8 @@ func (bc *bucketCache) Put(ctx context.Context, orgID valuer.UUID, q qbtypes.Que if endMs > fluxBoundary { cachableEndMs = fluxBoundary bc.logger.DebugContext(ctx, "adjusting end time to exclude flux interval", - "original_end", endMs, - "cachable_end", cachableEndMs) + slog.Uint64("original_end", endMs), + slog.Uint64("cachable_end", cachableEndMs)) } // Generate cache key @@ -155,11 +155,11 @@ func (bc *bucketCache) Put(ctx context.Context, orgID valuer.UUID, q qbtypes.Que // If after adjustment we have no complete intervals, don't cache if cachableStartMs >= cachableEndMs { bc.logger.DebugContext(ctx, "no complete intervals to cache", - "original_start", startMs, - "original_end", endMs, - "adjusted_start", cachableStartMs, - "adjusted_end", cachableEndMs, - "step", stepMs) + slog.Uint64("original_start", startMs), + slog.Uint64("original_end", endMs), + slog.Uint64("adjusted_start", cachableStartMs), + slog.Uint64("adjusted_end", cachableEndMs), + slog.Uint64("step", stepMs)) return } } @@ -187,7 +187,7 @@ func (bc *bucketCache) Put(ctx context.Context, orgID valuer.UUID, q qbtypes.Que // Marshal and store in cache if err := bc.cache.Set(ctx, orgID, cacheKey, &updatedData, bc.cacheTTL); err != nil { - bc.logger.ErrorContext(ctx, "error setting cached data", "error", err) + bc.logger.ErrorContext(ctx, "error setting cached data", errors.Attr(err)) } } @@ -471,7 +471,7 @@ func (bc *bucketCache) mergeTimeSeriesValues(ctx context.Context, buckets []*qbt for _, bucket := range buckets { var tsData *qbtypes.TimeSeriesData if err := json.Unmarshal(bucket.Value, &tsData); err != nil { - bc.logger.ErrorContext(ctx, "failed to unmarshal time series data", "error", err) + bc.logger.ErrorContext(ctx, "failed to unmarshal time series data", errors.Attr(err)) continue } @@ -623,7 +623,7 @@ func (bc *bucketCache) resultToBuckets(ctx context.Context, result *qbtypes.Resu // In the future, we could split large ranges into smaller buckets valueBytes, err := json.Marshal(result.Value) if err != nil { - bc.logger.ErrorContext(ctx, "failed to marshal result value", "error", err) + bc.logger.ErrorContext(ctx, "failed to marshal result value", errors.Attr(err)) return nil } diff --git a/pkg/querier/postprocess.go b/pkg/querier/postprocess.go index eb83a203a85..9b2dc48158a 100644 --- a/pkg/querier/postprocess.go +++ b/pkg/querier/postprocess.go @@ -3,12 +3,15 @@ package querier import ( "context" "fmt" + "log/slog" "math" "slices" "sort" "strings" "github.com/SigNoz/govaluate" + + "github.com/SigNoz/signoz/pkg/errors" qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" "github.com/SigNoz/signoz/pkg/types/telemetrytypes" ) @@ -358,14 +361,14 @@ func (q *querier) processTimeSeriesFormula( // Create formula evaluator evaluator, err := qbtypes.NewFormulaEvaluator(formula.Expression, canDefaultZero) if err != nil { - q.logger.ErrorContext(ctx, "failed to create formula evaluator", "error", err, "formula", formula.Name) + q.logger.ErrorContext(ctx, "failed to create formula evaluator", errors.Attr(err), slog.String("formula", formula.Name)) return nil } // Evaluate the formula formulaSeries, err := evaluator.EvaluateFormula(timeSeriesData) if err != nil { - q.logger.ErrorContext(ctx, "failed to evaluate formula", "error", err, "formula", formula.Name) + q.logger.ErrorContext(ctx, "failed to evaluate formula", errors.Attr(err), slog.String("formula", formula.Name)) return nil } @@ -508,13 +511,13 @@ func (q *querier) processScalarFormula( canDefaultZero := req.GetQueriesSupportingZeroDefault() evaluator, err := qbtypes.NewFormulaEvaluator(formula.Expression, canDefaultZero) if err != nil { - q.logger.ErrorContext(ctx, "failed to create formula evaluator", "error", err, "formula", formula.Name) + q.logger.ErrorContext(ctx, "failed to create formula evaluator", errors.Attr(err), slog.String("formula", formula.Name)) return nil } formulaSeries, err := evaluator.EvaluateFormula(timeSeriesData) if err != nil { - q.logger.ErrorContext(ctx, "failed to evaluate formula", "error", err, "formula", formula.Name) + q.logger.ErrorContext(ctx, "failed to evaluate formula", errors.Attr(err), slog.String("formula", formula.Name)) return nil } diff --git a/pkg/querier/promql_query.go b/pkg/querier/promql_query.go index abb365cfe31..c7355934fa7 100644 --- a/pkg/querier/promql_query.go +++ b/pkg/querier/promql_query.go @@ -11,6 +11,10 @@ import ( "text/template" "time" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql" + "github.com/prometheus/prometheus/promql/parser" + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/prometheus" "github.com/SigNoz/signoz/pkg/querybuilder" @@ -18,9 +22,6 @@ import ( "github.com/SigNoz/signoz/pkg/types/instrumentationtypes" qbv5 "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" "github.com/SigNoz/signoz/pkg/types/telemetrytypes" - "github.com/prometheus/prometheus/model/labels" - "github.com/prometheus/prometheus/promql" - "github.com/prometheus/prometheus/promql/parser" ) // unquotedDottedNamePattern matches unquoted identifiers containing dots @@ -92,7 +93,7 @@ func newPromqlQuery( func (q *promqlQuery) Fingerprint() string { query, err := q.renderVars(q.query.Query, q.vars, q.tr.From, q.tr.To) if err != nil { - q.logger.ErrorContext(context.TODO(), "failed render template variables", "query", q.query.Query) + q.logger.ErrorContext(context.TODO(), "failed render template variables", slog.String("query", q.query.Query)) return "" } parts := []string{ @@ -135,7 +136,7 @@ func (q *promqlQuery) removeAllVarMatchers(query string, vars map[string]qbv5.Va // Create visitor and walk the AST visitor := &allVarRemover{allVars: allVars} if err := parser.Walk(visitor, expr, nil); err != nil { - q.logger.ErrorContext(context.TODO(), "unexpected error while removing __all__ variable matchers", "error", err, "query", query) + q.logger.ErrorContext(context.TODO(), "unexpected error while removing __all__ variable matchers", errors.Attr(err), slog.String("query", query)) return "", errors.WrapInternalf(err, errors.CodeInternal, "error while removing __all__ variable matchers") } diff --git a/pkg/querier/querier.go b/pkg/querier/querier.go index 80dab965ece..992df13347e 100644 --- a/pkg/querier/querier.go +++ b/pkg/querier/querier.go @@ -10,6 +10,9 @@ import ( "sync" "time" + "github.com/dustin/go-humanize" + "golang.org/x/exp/maps" + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/prometheus" @@ -20,8 +23,6 @@ import ( "github.com/SigNoz/signoz/pkg/types/instrumentationtypes" "github.com/SigNoz/signoz/pkg/types/metrictypes" "github.com/SigNoz/signoz/pkg/types/telemetrytypes" - "github.com/dustin/go-humanize" - "golang.org/x/exp/maps" qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" "github.com/SigNoz/signoz/pkg/valuer" @@ -368,10 +369,10 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype var err error metricTemporality, metricTypes, err = q.metadataStore.FetchTemporalityAndTypeMulti(ctx, req.Start, req.End, metricNames...) if err != nil { - q.logger.WarnContext(ctx, "failed to fetch metric temporality", "error", err, "metrics", metricNames) + q.logger.WarnContext(ctx, "failed to fetch metric temporality", errors.Attr(err), slog.Any("metrics", metricNames)) return nil, errors.NewInternalf(errors.CodeInternal, "failed to fetch metrics temporality") } - q.logger.DebugContext(ctx, "fetched metric temporalities and types", "metric_temporality", metricTemporality, "metric_types", metricTypes) + q.logger.DebugContext(ctx, "fetched metric temporalities and types", slog.Any("metric_temporality", metricTemporality), slog.Any("metric_types", metricTypes)) } for i := range spec.Aggregations { if spec.Aggregations[i].MetricName != "" && spec.Aggregations[i].Temporality == metrictypes.Unknown { @@ -590,9 +591,9 @@ func (q *querier) run( // Skip cache if NoCache is set, or if cache is not available if req.NoCache || q.bucketCache == nil || query.Fingerprint() == "" { if req.NoCache { - q.logger.DebugContext(ctx, "NoCache flag set, bypassing cache", "query", name) + q.logger.DebugContext(ctx, "NoCache flag set, bypassing cache", slog.String("query", name)) } else { - q.logger.InfoContext(ctx, "no bucket cache or fingerprint, executing query", "fingerprint", query.Fingerprint()) + q.logger.InfoContext(ctx, "no bucket cache or fingerprint, executing query", slog.String("fingerprint", query.Fingerprint())) } result, err := query.Execute(ctx) qbEvent.HasData = qbEvent.HasData || hasData(result) @@ -709,8 +710,8 @@ func (q *querier) executeWithCache(ctx context.Context, orgID valuer.UUID, query totalStats := qbtypes.ExecStats{} q.logger.DebugContext(ctx, "executing queries for missing ranges", - "missing_ranges_count", len(missingRanges), - "ranges", missingRanges) + slog.Int("missing_ranges_count", len(missingRanges)), + slog.Any("ranges", missingRanges)) sem := make(chan struct{}, 4) var wg sync.WaitGroup @@ -748,7 +749,7 @@ func (q *querier) executeWithCache(ctx context.Context, orgID valuer.UUID, query for _, err := range errs { if err != nil { // If any query failed, fall back to full execution - q.logger.ErrorContext(ctx, "parallel query execution failed", "error", err) + q.logger.ErrorContext(ctx, "parallel query execution failed", errors.Attr(err)) result, err := query.Execute(ctx) if err != nil { return nil, err diff --git a/pkg/query-service/agentConf/db.go b/pkg/query-service/agentConf/db.go index 5299d66bbcf..92a6bd33da2 100644 --- a/pkg/query-service/agentConf/db.go +++ b/pkg/query-service/agentConf/db.go @@ -9,13 +9,14 @@ import ( "log/slog" + "golang.org/x/exp/slices" + "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" - "golang.org/x/exp/slices" ) var ( @@ -135,7 +136,7 @@ func (r *Repo) insertConfig( 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) + slog.ErrorContext(ctx, "failed to fetch latest config version", errors.Attr(err)) return err } @@ -155,11 +156,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()) + slog.ErrorContext(ctx, "failed to delete config elements during cleanup", errors.Attr(delErr), "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()) + slog.ErrorContext(ctx, "failed to delete config version during cleanup", errors.Attr(delErr), "version_id", c.ID.String()) } } }() @@ -170,7 +171,7 @@ func (r *Repo) insertConfig( Model(c). Exec(ctx) if dbErr != nil { - slog.ErrorContext(ctx, "error in inserting config version", "error", dbErr) + slog.ErrorContext(ctx, "error in inserting config version", errors.Attr(dbErr)) return errors.WrapInternalf(dbErr, CodeConfigVersionInsertFailed, "failed to insert config version") } @@ -221,7 +222,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) + slog.ErrorContext(ctx, "failed to update deploy status", errors.Attr(err)) return model.BadRequest(fmt.Errorf("failed to update deploy status")) } @@ -258,7 +259,7 @@ func (r *Repo) updateDeployStatusByHash( Where("org_id = ?", orgId). Exec(ctx) if err != nil { - slog.ErrorContext(ctx, "failed to update deploy status", "error", err) + slog.ErrorContext(ctx, "failed to update deploy status", errors.Attr(err)) return errors.WrapInternalf(err, CodeConfigDeployStatusUpdateFailed, "failed to update deploy status") } diff --git a/pkg/query-service/agentConf/manager.go b/pkg/query-service/agentConf/manager.go index d923751e0aa..2557c099def 100644 --- a/pkg/query-service/agentConf/manager.go +++ b/pkg/query-service/agentConf/manager.go @@ -9,6 +9,9 @@ import ( "sync" "sync/atomic" + "github.com/google/uuid" + yaml "gopkg.in/yaml.v3" + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/query-service/app/opamp" filterprocessor "github.com/SigNoz/signoz/pkg/query-service/app/opamp/otelconfig/filterprocessor" @@ -17,8 +20,6 @@ import ( "github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/types/opamptypes" "github.com/SigNoz/signoz/pkg/valuer" - "github.com/google/uuid" - yaml "gopkg.in/yaml.v3" ) var m *Manager @@ -185,7 +186,6 @@ func (m *Manager) GetDeployStatusByHash(ctx context.Context, orgId valuer.UUID, return m.Repo.GetDeployStatusByHash(ctx, orgId, configHash) } - func GetLatestVersion( ctx context.Context, orgId valuer.UUID, elementType opamptypes.ElementType, ) (*opamptypes.AgentConfigVersion, error) { @@ -230,7 +230,7 @@ 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) + slog.ErrorContext(ctx, "failed to fetch config version during redeploy", errors.Attr(err)) return err } @@ -242,7 +242,7 @@ func Redeploy(ctx context.Context, orgId valuer.UUID, typ opamptypes.ElementType 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) + slog.DebugContext(ctx, "failed to read last conf correctly", errors.Attr(err)) return model.BadRequest(fmt.Errorf("failed to read the stored config correctly")) } @@ -254,7 +254,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) + slog.ErrorContext(ctx, "failed to call agent config update for trace processor", errors.Attr(err)) return errors.WithAdditionalf(err, "failed to deploy the config") } @@ -262,7 +262,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) + slog.ErrorContext(ctx, "failed to read last conf correctly", errors.Attr(err)) return model.InternalError(fmt.Errorf("failed to read the stored config correctly")) } processorConf := map[string]interface{}{ @@ -272,7 +272,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) + slog.ErrorContext(ctx, "failed to call agent config update for trace processor", errors.Attr(err)) return err } @@ -298,13 +298,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) + slog.ErrorContext(ctx, "failed to call agent config update for trace processor", errors.Attr(err)) return err } processorConfYaml, yamlErr := yaml.Marshal(config) if yamlErr != nil { - slog.WarnContext(ctx, "unexpected error while transforming processor config to yaml", "error", yamlErr) + slog.WarnContext(ctx, "unexpected error while transforming processor config to yaml", errors.Attr(yamlErr)) } m.updateDeployStatus(ctx, orgId, opamptypes.ElementTypeDropRules, version, opamptypes.DeployInitiated.StringValue(), "Deployment started", configHash, string(processorConfYaml)) @@ -349,13 +349,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) + slog.ErrorContext(ctx, "failed to call agent config update for trace processor", errors.Attr(err)) return err } processorConfYaml, yamlErr := yaml.Marshal(config) if yamlErr != nil { - slog.WarnContext(ctx, "unexpected error while transforming processor config to yaml", "error", yamlErr) + slog.WarnContext(ctx, "unexpected error while transforming processor config to yaml", errors.Attr(yamlErr)) } m.updateDeployStatus(ctx, orgId, opamptypes.ElementTypeSamplingRules, version, opamptypes.DeployInitiated.StringValue(), "Deployment started", configHash, string(processorConfYaml)) diff --git a/pkg/query-service/app/clickhouseReader/filter_suggestions.go b/pkg/query-service/app/clickhouseReader/filter_suggestions.go index 1ad47baafd1..9f0893debfa 100644 --- a/pkg/query-service/app/clickhouseReader/filter_suggestions.go +++ b/pkg/query-service/app/clickhouseReader/filter_suggestions.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/SigNoz/signoz-otel-collector/utils/fingerprint" + errorsV2 "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/query-service/model" v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3" ) @@ -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) + r.logger.ErrorContext(ctx, "could not find attribute values for creating example query", errorsV2.Attr(err)) } else { // add example queries for as many attributes as possible. @@ -183,7 +184,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) + r.logger.ErrorContext(ctx, "couldn't query attrib values for suggestions", errorsV2.Attr(err)) return nil, model.InternalError(fmt.Errorf( "couldn't query attrib values for suggestions: %w", err, )) diff --git a/pkg/query-service/app/clickhouseReader/reader.go b/pkg/query-service/app/clickhouseReader/reader.go index 91b97a1f42f..a0706ebb233 100644 --- a/pkg/query-service/app/clickhouseReader/reader.go +++ b/pkg/query-service/app/clickhouseReader/reader.go @@ -15,6 +15,8 @@ import ( "sync" "time" + "github.com/uptrace/bun" + "github.com/SigNoz/signoz/pkg/prometheus" "github.com/SigNoz/signoz/pkg/query-service/model/metrics_explorer" "github.com/SigNoz/signoz/pkg/sqlstore" @@ -24,17 +26,18 @@ import ( "github.com/SigNoz/signoz/pkg/types/instrumentationtypes" "github.com/SigNoz/signoz/pkg/types/telemetrytypes" "github.com/SigNoz/signoz/pkg/valuer" - "github.com/uptrace/bun" - errorsV2 "github.com/SigNoz/signoz/pkg/errors" "github.com/google/uuid" "github.com/pkg/errors" + errorsV2 "github.com/SigNoz/signoz/pkg/errors" + "github.com/prometheus/prometheus/promql" "github.com/prometheus/prometheus/util/stats" "github.com/ClickHouse/clickhouse-go/v2" "github.com/ClickHouse/clickhouse-go/v2/lib/driver" + "github.com/SigNoz/signoz/pkg/cache" "log/slog" @@ -321,7 +324,7 @@ func (r *ClickHouseReader) GetTopLevelOperations(ctx context.Context, start, end rows, err := r.db.Query(ctx, query, clickhouse.Named("start", start), clickhouse.Named("services", services)) if err != nil { - r.logger.Error("Error in processing sql query", "error", err) + r.logger.Error("Error in processing sql query", errorsV2.Attr(err)) return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing sql query")} } @@ -393,7 +396,7 @@ func (r *ClickHouseReader) buildResourceSubQuery(tags []model.TagQueryParam, svc v3.AttributeKey{}, false) if err != nil { - r.logger.Error("Error in processing sql query", "error", err) + r.logger.Error("Error in processing sql query", errorsV2.Attr(err)) return "", err } return resourceSubQuery, nil @@ -473,7 +476,7 @@ func (r *ClickHouseReader) GetServices(ctx context.Context, queryParams *model.G resourceSubQuery, err := r.buildResourceSubQuery(queryParams.Tags, svc, *queryParams.Start, *queryParams.End) if err != nil { - r.logger.Error("Error in processing sql query", "error", err) + r.logger.Error("Error in processing sql query", errorsV2.Attr(err)) return } query += ` @@ -498,7 +501,7 @@ func (r *ClickHouseReader) GetServices(ctx context.Context, queryParams *model.G } if err != nil { - r.logger.Error("Error in processing sql query", "error", err) + r.logger.Error("Error in processing sql query", errorsV2.Attr(err)) return } @@ -510,7 +513,7 @@ func (r *ClickHouseReader) GetServices(ctx context.Context, queryParams *model.G err = r.db.QueryRow(ctx, errorQuery, args...).Scan(&numErrors) if err != nil { - r.logger.Error("Error in processing sql query", "error", err) + r.logger.Error("Error in processing sql query", errorsV2.Attr(err)) return } @@ -793,7 +796,7 @@ func (r *ClickHouseReader) GetTopOperations(ctx context.Context, queryParams *mo resourceSubQuery, err := r.buildResourceSubQuery(queryParams.Tags, queryParams.ServiceName, *queryParams.Start, *queryParams.End) if err != nil { - r.logger.Error("Error in processing sql query", "error", err) + r.logger.Error("Error in processing sql query", errorsV2.Attr(err)) return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing sql query")} } query += ` @@ -810,7 +813,7 @@ func (r *ClickHouseReader) GetTopOperations(ctx context.Context, queryParams *mo err = r.db.Select(ctx, &topOperationsItems, query, namedArgs...) if err != nil { - r.logger.Error("Error in processing sql query", "error", err) + r.logger.Error("Error in processing sql query", errorsV2.Attr(err)) return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing sql query")} } @@ -847,7 +850,7 @@ func (r *ClickHouseReader) GetUsage(ctx context.Context, queryParams *model.GetU r.logger.Info(query) if err != nil { - r.logger.Error("Error in processing sql query", "error", err) + r.logger.Error("Error in processing sql query", errorsV2.Attr(err)) return nil, fmt.Errorf("error in processing sql query") } @@ -877,7 +880,7 @@ func (r *ClickHouseReader) GetSpansForTrace(ctx context.Context, traceID string, if err == sql.ErrNoRows { return []model.SpanItemV2{}, nil } - r.logger.Error("Error in processing trace summary sql query", "error", err) + r.logger.Error("Error in processing trace summary sql query", errorsV2.Attr(err)) return nil, model.ExecutionError(fmt.Errorf("error in processing trace summary sql query: %w", err)) } @@ -886,7 +889,7 @@ func (r *ClickHouseReader) GetSpansForTrace(ctx context.Context, traceID string, err = r.db.Select(ctx, &searchScanResponses, traceDetailsQuery, traceID, strconv.FormatInt(traceSummary.Start.Unix()-1800, 10), strconv.FormatInt(traceSummary.End.Unix(), 10)) r.logger.Info(traceDetailsQuery) if err != nil { - r.logger.Error("Error in processing sql query", "error", err) + r.logger.Error("Error in processing sql query", errorsV2.Attr(err)) return nil, model.ExecutionError(fmt.Errorf("error in processing trace data sql query: %w", err)) } r.logger.Info("trace details query took: ", "duration", time.Since(queryStartTime), "traceID", traceID) @@ -898,7 +901,7 @@ func (r *ClickHouseReader) GetWaterfallSpansForTraceWithMetadataCache(ctx contex cachedTraceData := new(model.GetWaterfallSpansForTraceWithMetadataCache) err := r.cacheForTraceDetail.Get(ctx, orgID, strings.Join([]string{"getWaterfallSpansForTraceWithMetadata", traceID}, "-"), cachedTraceData) if err != nil { - r.logger.Debug("error in retrieving getWaterfallSpansForTraceWithMetadata cache", "error", err, "traceID", traceID) + r.logger.Debug("error in retrieving getWaterfallSpansForTraceWithMetadata cache", errorsV2.Attr(err), "traceID", traceID) return nil, err } @@ -949,7 +952,7 @@ func (r *ClickHouseReader) GetWaterfallSpansForTraceWithMetadata(ctx context.Con ref := []model.OtelSpanRef{} err := json.Unmarshal([]byte(item.References), &ref) if err != nil { - r.logger.Error("getWaterfallSpansForTraceWithMetadata: error unmarshalling references", "error", err, "traceID", traceID) + r.logger.Error("getWaterfallSpansForTraceWithMetadata: error unmarshalling references", errorsV2.Attr(err), "traceID", traceID) return nil, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, "getWaterfallSpansForTraceWithMetadata: error unmarshalling references %s", err.Error()) } @@ -969,7 +972,7 @@ func (r *ClickHouseReader) GetWaterfallSpansForTraceWithMetadata(ctx context.Con var eventMap model.Event err = json.Unmarshal([]byte(event), &eventMap) if err != nil { - r.logger.Error("Error unmarshalling events", "error", err) + r.logger.Error("Error unmarshalling events", errorsV2.Attr(err)) return nil, errorsV2.Newf(errorsV2.TypeInternal, errorsV2.CodeInternal, "getWaterfallSpansForTraceWithMetadata: error in unmarshalling events %s", err.Error()) } events = append(events, eventMap) @@ -1082,7 +1085,7 @@ func (r *ClickHouseReader) GetWaterfallSpansForTraceWithMetadata(ctx context.Con r.logger.Info("getWaterfallSpansForTraceWithMetadata: processing pre cache", "duration", time.Since(processingBeforeCache), "traceID", traceID) cacheErr := r.cacheForTraceDetail.Set(ctx, orgID, strings.Join([]string{"getWaterfallSpansForTraceWithMetadata", traceID}, "-"), &traceCache, time.Minute*5) if cacheErr != nil { - r.logger.Debug("failed to store cache for getWaterfallSpansForTraceWithMetadata", "traceID", traceID, "error", err) + r.logger.Debug("failed to store cache for getWaterfallSpansForTraceWithMetadata", "traceID", traceID, errorsV2.Attr(err)) } } @@ -1116,7 +1119,7 @@ func (r *ClickHouseReader) GetFlamegraphSpansForTraceCache(ctx context.Context, cachedTraceData := new(model.GetFlamegraphSpansForTraceCache) err := r.cacheForTraceDetail.Get(ctx, orgID, strings.Join([]string{"getFlamegraphSpansForTrace", traceID}, "-"), cachedTraceData) if err != nil { - r.logger.Debug("error in retrieving getFlamegraphSpansForTrace cache", "error", err, "traceID", traceID) + r.logger.Debug("error in retrieving getFlamegraphSpansForTrace cache", errorsV2.Attr(err), "traceID", traceID) return nil, err } @@ -1164,7 +1167,7 @@ func (r *ClickHouseReader) GetFlamegraphSpansForTrace(ctx context.Context, orgID ref := []model.OtelSpanRef{} err := json.Unmarshal([]byte(item.References), &ref) if err != nil { - r.logger.Error("Error unmarshalling references", "error", err) + r.logger.Error("Error unmarshalling references", errorsV2.Attr(err)) return nil, errorsV2.Newf(errorsV2.TypeInternal, errorsV2.CodeInternal, "getFlamegraphSpansForTrace: error in unmarshalling references %s", err.Error()) } @@ -1173,7 +1176,7 @@ func (r *ClickHouseReader) GetFlamegraphSpansForTrace(ctx context.Context, orgID var eventMap model.Event err = json.Unmarshal([]byte(event), &eventMap) if err != nil { - r.logger.Error("Error unmarshalling events", "error", err) + r.logger.Error("Error unmarshalling events", errorsV2.Attr(err)) return nil, errorsV2.Newf(errorsV2.TypeInternal, errorsV2.CodeInternal, "getFlamegraphSpansForTrace: error in unmarshalling events %s", err.Error()) } events = append(events, eventMap) @@ -1252,7 +1255,7 @@ func (r *ClickHouseReader) GetFlamegraphSpansForTrace(ctx context.Context, orgID r.logger.Info("getFlamegraphSpansForTrace: processing pre cache", "duration", time.Since(processingBeforeCache), "traceID", traceID) cacheErr := r.cacheForTraceDetail.Set(ctx, orgID, strings.Join([]string{"getFlamegraphSpansForTrace", traceID}, "-"), &traceCache, time.Minute*5) if cacheErr != nil { - r.logger.Debug("failed to store cache for getFlamegraphSpansForTrace", "traceID", traceID, "error", err) + r.logger.Debug("failed to store cache for getFlamegraphSpansForTrace", "traceID", traceID, errorsV2.Attr(err)) } } @@ -1312,7 +1315,7 @@ func (r *ClickHouseReader) GetDependencyGraph(ctx context.Context, queryParams * err := r.db.Select(ctx, &response, query, args...) if err != nil { - r.logger.Error("Error in processing sql query", "error", err) + r.logger.Error("Error in processing sql query", errorsV2.Attr(err)) return nil, fmt.Errorf("error in processing sql query %w", err) } @@ -1431,13 +1434,13 @@ func (r *ClickHouseReader) setTTLLogs(ctx context.Context, orgID string, params Model(&ttl). Exec(ctx) if dbErr != nil { - r.logger.Error("error in inserting to ttl_status table", "error", dbErr) + r.logger.Error("error in inserting to ttl_status table", errorsV2.Attr(dbErr)) return } err := r.setColdStorage(context.Background(), tableName, params.ColdStorageVolume) if err != nil { - r.logger.Error("error in setting cold storage", "error", err) + r.logger.Error("error in setting cold storage", errorsV2.Attr(err)) statusItem, apiErr := r.checkTTLStatusItem(ctx, orgID, tableName) if apiErr == nil { _, dbErr := r. @@ -1450,7 +1453,7 @@ func (r *ClickHouseReader) setTTLLogs(ctx context.Context, orgID string, params Where("id = ?", statusItem.ID.StringValue()). Exec(ctx) if dbErr != nil { - r.logger.Error("Error in processing ttl_status update sql query", "error", dbErr) + r.logger.Error("Error in processing ttl_status update sql query", errorsV2.Attr(dbErr)) return } } @@ -1459,7 +1462,7 @@ func (r *ClickHouseReader) setTTLLogs(ctx context.Context, orgID string, params r.logger.Info("Executing TTL request: ", "request", query) statusItem, _ := r.checkTTLStatusItem(ctx, orgID, tableName) if err := r.db.Exec(ctx, query); err != nil { - r.logger.Error("error while setting ttl", "error", err) + r.logger.Error("error while setting ttl", errorsV2.Attr(err)) _, dbErr := r. sqlDB. BunDB(). @@ -1470,7 +1473,7 @@ func (r *ClickHouseReader) setTTLLogs(ctx context.Context, orgID string, params Where("id = ?", statusItem.ID.StringValue()). Exec(ctx) if dbErr != nil { - r.logger.Error("Error in processing ttl_status update sql query", "error", dbErr) + r.logger.Error("Error in processing ttl_status update sql query", errorsV2.Attr(dbErr)) return } return @@ -1485,7 +1488,7 @@ func (r *ClickHouseReader) setTTLLogs(ctx context.Context, orgID string, params Where("id = ?", statusItem.ID.StringValue()). Exec(ctx) if dbErr != nil { - r.logger.Error("Error in processing ttl_status update sql query", "error", dbErr) + r.logger.Error("Error in processing ttl_status update sql query", errorsV2.Attr(dbErr)) return } } @@ -1569,7 +1572,7 @@ func (r *ClickHouseReader) setTTLTraces(ctx context.Context, orgID string, param Model(&ttl). Exec(ctx) if dbErr != nil { - r.logger.Error("error in inserting to ttl_status table", "error", dbErr) + r.logger.Error("error in inserting to ttl_status table", errorsV2.Attr(dbErr)) return } @@ -1587,7 +1590,7 @@ func (r *ClickHouseReader) setTTLTraces(ctx context.Context, orgID string, param } err := r.setColdStorage(context.Background(), tableName, params.ColdStorageVolume) if err != nil { - r.logger.Error("Error in setting cold storage", "error", err) + r.logger.Error("Error in setting cold storage", errorsV2.Attr(err)) statusItem, apiErr := r.checkTTLStatusItem(ctx, orgID, tableName) if apiErr == nil { _, dbErr := r. @@ -1600,7 +1603,7 @@ func (r *ClickHouseReader) setTTLTraces(ctx context.Context, orgID string, param Where("id = ?", statusItem.ID.StringValue()). Exec(ctx) if dbErr != nil { - r.logger.Error("Error in processing ttl_status update sql query", "error", dbErr) + r.logger.Error("Error in processing ttl_status update sql query", errorsV2.Attr(dbErr)) return } } @@ -1610,7 +1613,7 @@ func (r *ClickHouseReader) setTTLTraces(ctx context.Context, orgID string, param r.logger.Error(" ExecutingTTL request: ", "request", req) statusItem, _ := r.checkTTLStatusItem(ctx, orgID, tableName) if err := r.db.Exec(ctx, req); err != nil { - r.logger.Error("Error in executing set TTL query", "error", err) + r.logger.Error("Error in executing set TTL query", errorsV2.Attr(err)) _, dbErr := r. sqlDB. BunDB(). @@ -1621,7 +1624,7 @@ func (r *ClickHouseReader) setTTLTraces(ctx context.Context, orgID string, param Where("id = ?", statusItem.ID.StringValue()). Exec(ctx) if dbErr != nil { - r.logger.Error("Error in processing ttl_status update sql query", "error", dbErr) + r.logger.Error("Error in processing ttl_status update sql query", errorsV2.Attr(dbErr)) return } return @@ -1636,7 +1639,7 @@ func (r *ClickHouseReader) setTTLTraces(ctx context.Context, orgID string, param Where("id = ?", statusItem.ID.StringValue()). Exec(ctx) if dbErr != nil { - r.logger.Error("Error in processing ttl_status update sql query", "error", dbErr) + r.logger.Error("Error in processing ttl_status update sql query", errorsV2.Attr(dbErr)) return } }(distributedTableName) @@ -1662,7 +1665,7 @@ func (r *ClickHouseReader) hasCustomRetentionColumn(ctx context.Context) (bool, r.logger.Debug("_retention_days column not found in logs table", "table", r.logsLocalTableV2) return false, nil } - r.logger.Error("Error checking for _retention_days column", "error", err) + r.logger.Error("Error checking for _retention_days column", errorsV2.Attr(err)) return false, errorsV2.Wrapf(err, errorsV2.TypeInternal, errorsV2.CodeInternal, "error checking columns") } @@ -1842,14 +1845,14 @@ func (r *ClickHouseReader) SetTTLV2(ctx context.Context, orgID string, params *m // Insert TTL setting record _, dbErr := r.sqlDB.BunDB().NewInsert().Model(&customTTL).Exec(ctx) if dbErr != nil { - r.logger.Error("error in inserting to custom_retention_ttl_settings table", "error", dbErr) + r.logger.Error("error in inserting to custom_retention_ttl_settings table", errorsV2.Attr(dbErr)) return nil, errorsV2.Wrapf(dbErr, errorsV2.TypeInternal, errorsV2.CodeInternal, "error inserting TTL settings") } if len(params.ColdStorageVolume) > 0 && coldStorageDuration > 0 { err := r.setColdStorage(ctx, tableName, params.ColdStorageVolume) if err != nil { - r.logger.Error("error in setting cold storage", "error", err) + r.logger.Error("error in setting cold storage", errorsV2.Attr(err)) r.updateCustomRetentionTTLStatus(ctx, orgID, tableName, constants.StatusFailed) return nil, errorsV2.Wrapf(err.Err, errorsV2.TypeInternal, errorsV2.CodeInternal, "error setting cold storage for table %s", tableName) } @@ -1858,7 +1861,7 @@ func (r *ClickHouseReader) SetTTLV2(ctx context.Context, orgID string, params *m for i, query := range queries { r.logger.Debug("Executing custom retention TTL request: ", "request", query, "step", i+1) if err := r.db.Exec(ctx, query); err != nil { - r.logger.Error("error while setting custom retention ttl", "error", err) + r.logger.Error("error while setting custom retention ttl", errorsV2.Attr(err)) r.updateCustomRetentionTTLStatus(ctx, orgID, tableName, constants.StatusFailed) return nil, errorsV2.Wrapf(err, errorsV2.TypeInternal, errorsV2.CodeInternal, "error setting custom retention TTL for table %s, query: %s", tableName, query) } @@ -1950,7 +1953,7 @@ func (r *ClickHouseReader) GetCustomRetentionTTL(ctx context.Context, orgID stri hasCustomRetention, err := r.hasCustomRetentionColumn(ctx) if err != nil { // If there's an error checking, assume V1 and proceed - r.logger.Warn("Error checking for custom retention column, assuming V1", "error", err) + r.logger.Warn("Error checking for custom retention column, assuming V1", errorsV2.Attr(err)) hasCustomRetention = false } @@ -1971,7 +1974,7 @@ func (r *ClickHouseReader) GetCustomRetentionTTL(ctx context.Context, orgID stri Scan(ctx) if err != nil && err != sql.ErrNoRows { - r.logger.Error("Error in processing sql query", "error", err) + r.logger.Error("Error in processing sql query", errorsV2.Attr(err)) return nil, errorsV2.Newf(errorsV2.TypeInternal, errorsV2.CodeInternal, "error in processing get custom ttl query") } @@ -1988,7 +1991,7 @@ func (r *ClickHouseReader) GetCustomRetentionTTL(ctx context.Context, orgID stri var ttlConditions []model.CustomRetentionRule if customTTL.Condition != "" { if err := json.Unmarshal([]byte(customTTL.Condition), &ttlConditions); err != nil { - r.logger.Error("Error parsing TTL conditions", "error", err) + r.logger.Error("Error parsing TTL conditions", errorsV2.Attr(err)) ttlConditions = []model.CustomRetentionRule{} } } @@ -2041,7 +2044,7 @@ func (r *ClickHouseReader) checkCustomRetentionTTLStatusItem(ctx context.Context Scan(ctx) if err != nil && err != sql.ErrNoRows { - r.logger.Error("Error in processing sql query", "error", err) + r.logger.Error("Error in processing sql query", errorsV2.Attr(err)) return ttl, errorsV2.Newf(errorsV2.TypeInternal, errorsV2.CodeInternal, "error in processing custom_retention_ttl_status check sql query") } @@ -2058,7 +2061,7 @@ func (r *ClickHouseReader) updateCustomRetentionTTLStatus(ctx context.Context, o Where("id = ?", statusItem.ID.StringValue()). Exec(ctx) if dbErr != nil { - r.logger.Error("Error in processing custom_retention_ttl_status update sql query", "error", dbErr) + r.logger.Error("Error in processing custom_retention_ttl_status update sql query", errorsV2.Attr(dbErr)) } } } @@ -2241,7 +2244,7 @@ func (r *ClickHouseReader) setTTLMetrics(ctx context.Context, orgID string, para Model(&ttl). Exec(ctx) if dbErr != nil { - r.logger.Error("error in inserting to ttl_status table", "error", dbErr) + r.logger.Error("error in inserting to ttl_status table", errorsV2.Attr(dbErr)) return } timeColumn := "timestamp_ms" @@ -2259,7 +2262,7 @@ func (r *ClickHouseReader) setTTLMetrics(ctx context.Context, orgID string, para } err := r.setColdStorage(context.Background(), tableName, params.ColdStorageVolume) if err != nil { - r.logger.Error("Error in setting cold storage", "error", err) + r.logger.Error("Error in setting cold storage", errorsV2.Attr(err)) statusItem, apiErr := r.checkTTLStatusItem(ctx, orgID, tableName) if apiErr == nil { _, dbErr := r. @@ -2272,7 +2275,7 @@ func (r *ClickHouseReader) setTTLMetrics(ctx context.Context, orgID string, para Where("id = ?", statusItem.ID.StringValue()). Exec(ctx) if dbErr != nil { - r.logger.Error("Error in processing ttl_status update sql query", "error", dbErr) + r.logger.Error("Error in processing ttl_status update sql query", errorsV2.Attr(dbErr)) return } } @@ -2282,7 +2285,7 @@ func (r *ClickHouseReader) setTTLMetrics(ctx context.Context, orgID string, para r.logger.Info("Executing TTL request: ", "request", req) statusItem, _ := r.checkTTLStatusItem(ctx, orgID, tableName) if err := r.db.Exec(ctx, req); err != nil { - r.logger.Error("error while setting ttl.", "error", err) + r.logger.Error("error while setting ttl.", errorsV2.Attr(err)) _, dbErr := r. sqlDB. BunDB(). @@ -2293,7 +2296,7 @@ func (r *ClickHouseReader) setTTLMetrics(ctx context.Context, orgID string, para Where("id = ?", statusItem.ID.StringValue()). Exec(ctx) if dbErr != nil { - r.logger.Error("Error in processing ttl_status update sql query", "error", dbErr) + r.logger.Error("Error in processing ttl_status update sql query", errorsV2.Attr(dbErr)) return } return @@ -2308,7 +2311,7 @@ func (r *ClickHouseReader) setTTLMetrics(ctx context.Context, orgID string, para Where("id = ?", statusItem.ID.StringValue()). Exec(ctx) if dbErr != nil { - r.logger.Error("Error in processing ttl_status update sql query", "error", dbErr) + r.logger.Error("Error in processing ttl_status update sql query", errorsV2.Attr(dbErr)) return } } @@ -2333,7 +2336,7 @@ func (r *ClickHouseReader) deleteTtlTransactions(ctx context.Context, orgID stri Scan(ctx, &limitTransactions) if err != nil { - r.logger.Error("Error in processing ttl_status delete sql query", "error", err) + r.logger.Error("Error in processing ttl_status delete sql query", errorsV2.Attr(err)) } _, err = r. @@ -2344,7 +2347,7 @@ func (r *ClickHouseReader) deleteTtlTransactions(ctx context.Context, orgID stri Where("transaction_id NOT IN (?)", bun.In(limitTransactions)). Exec(ctx) if err != nil { - r.logger.Error("Error in processing ttl_status delete sql query", "error", err) + r.logger.Error("Error in processing ttl_status delete sql query", errorsV2.Attr(err)) } } @@ -2363,7 +2366,7 @@ func (r *ClickHouseReader) checkTTLStatusItem(ctx context.Context, orgID string, Limit(1). Scan(ctx) if err != nil && err != sql.ErrNoRows { - r.logger.Error("Error in processing sql query", "error", err) + r.logger.Error("Error in processing sql query", errorsV2.Attr(err)) return ttl, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing ttl_status check sql query")} } return ttl, nil @@ -2410,7 +2413,7 @@ func (r *ClickHouseReader) setColdStorage(ctx context.Context, tableName string, r.logger.Info("Executing Storage policy request: ", "request", policyReq) if err := r.db.Exec(ctx, policyReq); err != nil { - r.logger.Error("error while setting storage policy", "error", err) + r.logger.Error("error while setting storage policy", errorsV2.Attr(err)) return &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error while setting storage policy. Err=%v", err)} } } @@ -2427,7 +2430,7 @@ func (r *ClickHouseReader) GetDisks(ctx context.Context) (*[]model.DiskItem, *mo query := "SELECT name,type FROM system.disks" if err := r.db.Select(ctx, &diskItems, query); err != nil { - r.logger.Error("Error in processing sql query", "error", err) + r.logger.Error("Error in processing sql query", errorsV2.Attr(err)) return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error while getting disks. Err=%v", err)} } @@ -2487,7 +2490,7 @@ func (r *ClickHouseReader) GetTTL(ctx context.Context, orgID string, ttlParams * err := r.db.Select(ctx, &dbResp, query) if err != nil { - r.logger.Error("error while getting ttl", "error", err) + r.logger.Error("error while getting ttl", errorsV2.Attr(err)) return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error while getting ttl. Err=%v", err)} } if len(dbResp) == 0 { @@ -2505,7 +2508,7 @@ func (r *ClickHouseReader) GetTTL(ctx context.Context, orgID string, ttlParams * err := r.db.Select(ctx, &dbResp, query) if err != nil { - r.logger.Error("error while getting ttl", "error", err) + r.logger.Error("error while getting ttl", errorsV2.Attr(err)) return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error while getting ttl. Err=%v", err)} } if len(dbResp) == 0 { @@ -2523,7 +2526,7 @@ func (r *ClickHouseReader) GetTTL(ctx context.Context, orgID string, ttlParams * err := r.db.Select(ctx, &dbResp, query) if err != nil { - r.logger.Error("error while getting ttl", "error", err) + r.logger.Error("error while getting ttl", errorsV2.Attr(err)) return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error while getting ttl. Err=%v", err)} } if len(dbResp) == 0 { @@ -2656,7 +2659,7 @@ func (r *ClickHouseReader) ListErrors(ctx context.Context, queryParams *model.Li args = append(args, argsSubQuery...) if errStatus != nil { - r.logger.Error("Error in processing tags", "error", errStatus) + r.logger.Error("Error in processing tags", errorsV2.Attr(errStatus)) return nil, errStatus } query = query + " GROUP BY groupID" @@ -2687,7 +2690,7 @@ func (r *ClickHouseReader) ListErrors(ctx context.Context, queryParams *model.Li r.logger.Info(query) if err != nil { - r.logger.Error("Error in processing sql query", "error", err) + r.logger.Error("Error in processing sql query", errorsV2.Attr(err)) return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing sql query")} } @@ -2722,7 +2725,7 @@ func (r *ClickHouseReader) CountErrors(ctx context.Context, queryParams *model.C args = append(args, argsSubQuery...) if errStatus != nil { - r.logger.Error("Error in processing tags", "error", errStatus) + r.logger.Error("Error in processing tags", errorsV2.Attr(errStatus)) return 0, errStatus } @@ -2730,7 +2733,7 @@ func (r *ClickHouseReader) CountErrors(ctx context.Context, queryParams *model.C r.logger.Info(query) if err != nil { - r.logger.Error("Error in processing sql query", "error", err) + r.logger.Error("Error in processing sql query", errorsV2.Attr(err)) return 0, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing sql query")} } @@ -2757,7 +2760,7 @@ func (r *ClickHouseReader) GetErrorFromErrorID(ctx context.Context, queryParams r.logger.Info(query) if err != nil { - r.logger.Error("Error in processing sql query", "error", err) + r.logger.Error("Error in processing sql query", errorsV2.Attr(err)) return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing sql query")} } @@ -2786,7 +2789,7 @@ func (r *ClickHouseReader) GetErrorFromGroupID(ctx context.Context, queryParams r.logger.Info(query) if err != nil { - r.logger.Error("Error in processing sql query", "error", err) + r.logger.Error("Error in processing sql query", errorsV2.Attr(err)) return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing sql query")} } @@ -2810,12 +2813,12 @@ func (r *ClickHouseReader) GetNextPrevErrorIDs(ctx context.Context, queryParams } getNextPrevErrorIDsResponse.NextErrorID, getNextPrevErrorIDsResponse.NextTimestamp, apiErr = r.getNextErrorID(ctx, queryParams) if apiErr != nil { - r.logger.Error("Unable to get next error ID due to err: ", "error", apiErr) + r.logger.Error("Unable to get next error ID due to err: ", errorsV2.Attr(apiErr)) return nil, apiErr } getNextPrevErrorIDsResponse.PrevErrorID, getNextPrevErrorIDsResponse.PrevTimestamp, apiErr = r.getPrevErrorID(ctx, queryParams) if apiErr != nil { - r.logger.Error("Unable to get prev error ID due to err: ", "error", apiErr) + r.logger.Error("Unable to get prev error ID due to err: ", errorsV2.Attr(apiErr)) return nil, apiErr } return &getNextPrevErrorIDsResponse, nil @@ -2839,7 +2842,7 @@ func (r *ClickHouseReader) getNextErrorID(ctx context.Context, queryParams *mode r.logger.Info(query) if err != nil { - r.logger.Error("Error in processing sql query", "error", err) + r.logger.Error("Error in processing sql query", errorsV2.Attr(err)) return "", time.Time{}, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing sql query")} } if len(getNextErrorIDReponse) == 0 { @@ -2860,7 +2863,7 @@ func (r *ClickHouseReader) getNextErrorID(ctx context.Context, queryParams *mode r.logger.Info(query) if err != nil { - r.logger.Error("Error in processing sql query", "error", err) + r.logger.Error("Error in processing sql query", errorsV2.Attr(err)) return "", time.Time{}, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing sql query")} } if len(getNextErrorIDReponse) == 0 { @@ -2874,7 +2877,7 @@ func (r *ClickHouseReader) getNextErrorID(ctx context.Context, queryParams *mode r.logger.Info(query) if err != nil { - r.logger.Error("Error in processing sql query", "error", err) + r.logger.Error("Error in processing sql query", errorsV2.Attr(err)) return "", time.Time{}, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing sql query")} } @@ -2913,7 +2916,7 @@ func (r *ClickHouseReader) getPrevErrorID(ctx context.Context, queryParams *mode r.logger.Info(query) if err != nil { - r.logger.Error("Error in processing sql query", "error", err) + r.logger.Error("Error in processing sql query", errorsV2.Attr(err)) return "", time.Time{}, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing sql query")} } if len(getPrevErrorIDReponse) == 0 { @@ -2934,7 +2937,7 @@ func (r *ClickHouseReader) getPrevErrorID(ctx context.Context, queryParams *mode r.logger.Info(query) if err != nil { - r.logger.Error("Error in processing sql query", "error", err) + r.logger.Error("Error in processing sql query", errorsV2.Attr(err)) return "", time.Time{}, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing sql query")} } if len(getPrevErrorIDReponse) == 0 { @@ -2948,7 +2951,7 @@ func (r *ClickHouseReader) getPrevErrorID(ctx context.Context, queryParams *mode r.logger.Info(query) if err != nil { - r.logger.Error("Error in processing sql query", "error", err) + r.logger.Error("Error in processing sql query", errorsV2.Attr(err)) return "", time.Time{}, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing sql query")} } @@ -2976,7 +2979,7 @@ func (r *ClickHouseReader) FetchTemporality(ctx context.Context, orgID valuer.UU // Batch fetch all metadata at once metadataMap, apiErr := r.GetUpdatedMetricsMetadata(ctx, orgID, metricNames...) if apiErr != nil { - r.logger.Warn("Failed to fetch updated metrics metadata", "error", apiErr) + r.logger.Warn("Failed to fetch updated metrics metadata", errorsV2.Attr(apiErr)) return nil, apiErr } @@ -3325,7 +3328,7 @@ func (r *ClickHouseReader) QueryDashboardVars(ctx context.Context, query string) r.logger.Info(query) if err != nil { - r.logger.Error("Error in processing sql query", "error", err) + r.logger.Error("Error in processing sql query", errorsV2.Attr(err)) return nil, err } @@ -3379,7 +3382,7 @@ func (r *ClickHouseReader) GetMetricAggregateAttributes(ctx context.Context, org rows, err := r.db.Query(ctx, query, fmt.Sprintf("%%%s%%", req.SearchText), normalized) if err != nil { - r.logger.Error("Error while querying metric names", "error", err) + r.logger.Error("Error while querying metric names", errorsV2.Attr(err)) return nil, fmt.Errorf("error while executing metric name query: %s", err.Error()) } defer rows.Close() @@ -3458,7 +3461,7 @@ func (r *ClickHouseReader) GetMeterAggregateAttributes(ctx context.Context, orgI rows, err := r.db.Query(ctx, query, fmt.Sprintf("%%%s%%", req.SearchText)) if err != nil { - r.logger.Error("Error while querying meter names", "error", err) + r.logger.Error("Error while querying meter names", errorsV2.Attr(err)) return nil, fmt.Errorf("error while executing meter name query: %s", err.Error()) } defer rows.Close() @@ -3513,7 +3516,7 @@ func (r *ClickHouseReader) GetMetricAttributeKeys(ctx context.Context, req *v3.F } rows, err = r.db.Query(ctx, query, req.AggregateAttribute, common.PastDayRoundOff(), normalized, fmt.Sprintf("%%%s%%", req.SearchText)) if err != nil { - r.logger.Error("Error while executing query", "error", err) + r.logger.Error("Error while executing query", errorsV2.Attr(err)) return nil, fmt.Errorf("error while executing query: %s", err.Error()) } defer rows.Close() @@ -3553,7 +3556,7 @@ func (r *ClickHouseReader) GetMeterAttributeKeys(ctx context.Context, req *v3.Fi } rows, err = r.db.Query(ctx, query, req.AggregateAttribute, fmt.Sprintf("%%%s%%", req.SearchText)) if err != nil { - r.logger.Error("Error while executing query", "error", err) + r.logger.Error("Error while executing query", errorsV2.Attr(err)) return nil, fmt.Errorf("error while executing query: %s", err.Error()) } defer rows.Close() @@ -3602,7 +3605,7 @@ func (r *ClickHouseReader) GetMetricAttributeValues(ctx context.Context, req *v3 rows, err = r.db.Query(ctx, query, req.FilterAttributeKey, names, req.FilterAttributeKey, fmt.Sprintf("%%%s%%", req.SearchText), common.PastDayRoundOff(), normalized) if err != nil { - r.logger.Error("Error while executing query", "error", err) + r.logger.Error("Error while executing query", errorsV2.Attr(err)) return nil, fmt.Errorf("error while executing query: %s", err.Error()) } defer rows.Close() @@ -3632,7 +3635,7 @@ func (r *ClickHouseReader) GetMetricMetadata(ctx context.Context, orgID valuer.U // 1. Fetch metadata from cache/db using unified function metadataMap, apiError := r.GetUpdatedMetricsMetadata(ctx, orgID, metricName) if apiError != nil { - r.logger.Error("Error in getting metric cached metadata", "error", apiError) + r.logger.Error("Error in getting metric cached metadata", errorsV2.Attr(apiError)) return nil, fmt.Errorf("error fetching metric metadata: %s", apiError.Err.Error()) } @@ -3675,7 +3678,7 @@ func (r *ClickHouseReader) GetMetricMetadata(ctx context.Context, orgID valuer.U rows, err := r.db.Query(ctx, query, metricName, unixMilli, serviceName, serviceName) if err != nil { - r.logger.Error("Error while querying histogram buckets", "error", err) + r.logger.Error("Error while querying histogram buckets", errorsV2.Attr(err)) return nil, fmt.Errorf("error while querying histogram buckets: %s", err.Error()) } defer rows.Close() @@ -3687,7 +3690,7 @@ func (r *ClickHouseReader) GetMetricMetadata(ctx context.Context, orgID valuer.U } le, err := strconv.ParseFloat(leStr, 64) if err != nil || math.IsInf(le, 0) { - r.logger.Error("Invalid 'le' bucket value", "value", leStr, "error", err) + r.logger.Error("Invalid 'le' bucket value", "value", leStr, errorsV2.Attr(err)) continue } leFloat64 = append(leFloat64, le) @@ -3900,7 +3903,7 @@ func (r *ClickHouseReader) GetLogAggregateAttributes(ctx context.Context, req *v query = fmt.Sprintf("SELECT DISTINCT(tag_key), tag_type, tag_data_type from %s.%s WHERE %s and tag_type != 'logfield' limit $2", r.logsDB, r.logsTagAttributeTableV2, where) rows, err = r.db.Query(ctx, query, fmt.Sprintf("%%%s%%", req.SearchText), req.Limit) if err != nil { - r.logger.Error("Error while executing query", "error", err) + r.logger.Error("Error while executing query", errorsV2.Attr(err)) return nil, fmt.Errorf("error while executing query: %s", err.Error()) } defer rows.Close() @@ -3964,7 +3967,7 @@ func (r *ClickHouseReader) GetLogAttributeKeys(ctx context.Context, req *v3.Filt } if err != nil { - r.logger.Error("Error while executing query", "error", err) + r.logger.Error("Error while executing query", errorsV2.Attr(err)) return nil, fmt.Errorf("error while executing query: %s", err.Error()) } defer rows.Close() @@ -4179,7 +4182,7 @@ func (r *ClickHouseReader) GetLogAttributeValues(ctx context.Context, req *v3.Fi } if err != nil { - r.logger.Error("Error while executing query", "error", err) + r.logger.Error("Error while executing query", errorsV2.Attr(err)) return nil, fmt.Errorf("error while executing query: %s", err.Error()) } defer rows.Close() @@ -4249,7 +4252,7 @@ func readRow(vars []interface{}, columnNames []string, countOfNumberCols int) ([ var metric map[string]string err := json.Unmarshal([]byte(*v), &metric) if err != nil { - slog.Error("unexpected error encountered", "error", err) + slog.Error("unexpected error encountered", errorsV2.Attr(err)) } for key, val := range metric { groupBy = append(groupBy, val) @@ -4445,7 +4448,7 @@ func (r *ClickHouseReader) GetTimeSeriesResultV3(ctx context.Context, query stri go func() { err := r.queryProgressTracker.ReportQueryProgress(qid, p) if err != nil { - r.logger.Error("Couldn't report query progress", "queryId", qid, "error", err) + r.logger.Error("Couldn't report query progress", "queryId", qid, errorsV2.Attr(err)) } }() }, @@ -4456,7 +4459,7 @@ func (r *ClickHouseReader) GetTimeSeriesResultV3(ctx context.Context, query stri rows, err := r.db.Query(ctx, query) if err != nil { - r.logger.Error("error while reading time series result", "error", err) + r.logger.Error("error while reading time series result", errorsV2.Attr(err)) return nil, errors.New(err.Error()) } defer rows.Close() @@ -4498,7 +4501,7 @@ func (r *ClickHouseReader) GetListResultV3(ctx context.Context, query string) ([ }) rows, err := r.db.Query(ctx, query) if err != nil { - r.logger.Error("error while reading time series result", "error", err) + r.logger.Error("error while reading time series result", errorsV2.Attr(err)) return nil, errors.New(err.Error()) } @@ -4576,7 +4579,7 @@ func (r *ClickHouseReader) GetMetricsExistenceAndEarliestTime(ctx context.Contex var count, minFirstReported uint64 err := r.db.QueryRow(ctx, query, clickhouse.Named("metric_names", metricNames)).Scan(&count, &minFirstReported) if err != nil { - r.logger.Error("error getting host metrics existence and earliest time", "error", err) + r.logger.Error("error getting host metrics existence and earliest time", errorsV2.Attr(err)) return 0, 0, err } return count, minFirstReported, nil @@ -4586,7 +4589,7 @@ func getPersonalisedError(err error) error { if err == nil { return nil } - slog.Error("error while reading result", "error", err) + slog.Error("error while reading result", errorsV2.Attr(err)) if strings.Contains(err.Error(), "code: 307") { return chErrors.ErrResourceBytesLimitExceeded } @@ -4664,7 +4667,7 @@ func (r *ClickHouseReader) GetTraceAggregateAttributes(ctx context.Context, req rows, err = r.db.Query(ctx, query, fmt.Sprintf("%%%s%%", req.SearchText)) if err != nil { - r.logger.Error("Error while executing query", "error", err) + r.logger.Error("Error while executing query", errorsV2.Attr(err)) return nil, fmt.Errorf("error while executing query: %s", err.Error()) } defer rows.Close() @@ -4730,7 +4733,7 @@ func (r *ClickHouseReader) GetTraceAttributeKeys(ctx context.Context, req *v3.Fi rows, err = r.db.Query(ctx, query, fmt.Sprintf("%%%s%%", req.SearchText), req.Limit) if err != nil { - r.logger.Error("Error while executing query", "error", err) + r.logger.Error("Error while executing query", errorsV2.Attr(err)) return nil, fmt.Errorf("error while executing query: %s", err.Error()) } defer rows.Close() @@ -4846,7 +4849,7 @@ func (r *ClickHouseReader) GetTraceAttributeValues(ctx context.Context, req *v3. } if err != nil { - r.logger.Error("Error while executing query", "error", err) + r.logger.Error("Error while executing query", errorsV2.Attr(err)) return nil, fmt.Errorf("error while executing query: %s", err.Error()) } defer rows.Close() @@ -4895,7 +4898,7 @@ func (r *ClickHouseReader) GetSpanAttributeKeysByNames(ctx context.Context, name rows, err = r.db.Query(ctx, query) if err != nil { - r.logger.Error("Error while executing query", "error", err) + r.logger.Error("Error while executing query", errorsV2.Attr(err)) return nil, fmt.Errorf("error while executing query: %s", err.Error()) } defer rows.Close() @@ -5056,7 +5059,7 @@ func (r *ClickHouseReader) ReadRuleStateHistoryByRuleID( r.logger.Debug("rule state history query", "query", query) err := r.db.Select(ctx, &history, query) if err != nil { - r.logger.Error("Error while reading rule state history", "error", err) + r.logger.Error("Error while reading rule state history", errorsV2.Attr(err)) return nil, err } @@ -5124,7 +5127,7 @@ func (r *ClickHouseReader) ReadRuleStateHistoryTopContributorsByRuleID( contributors := []model.RuleStateHistoryContributor{} err := r.db.Select(ctx, &contributors, query) if err != nil { - r.logger.Error("Error while reading rule state history", "error", err) + r.logger.Error("Error while reading rule state history", errorsV2.Attr(err)) return nil, err } @@ -5425,7 +5428,7 @@ func (r *ClickHouseReader) GetMinAndMaxTimestampForTraceID(ctx context.Context, err := r.db.QueryRow(ctx, query).Scan(&minTime, &maxTime) if err != nil { - r.logger.Error("Error while executing query", "error", err) + r.logger.Error("Error while executing query", errorsV2.Attr(err)) return 0, 0, err } @@ -5471,7 +5474,7 @@ func (r *ClickHouseReader) GetAllMetricFilterAttributeKeys(ctx context.Context, valueCtx := context.WithValue(ctx, "clickhouse_max_threads", constants.MetricsExplorerClickhouseThreads) rows, err := r.db.Query(valueCtx, query, common.PastDayRoundOff(), normalized, fmt.Sprintf("%%%s%%", req.SearchText)) //only showing past day data if err != nil { - r.logger.Error("Error while executing query", "error", err) + r.logger.Error("Error while executing query", errorsV2.Attr(err)) return nil, &model.ApiError{Typ: "ClickHouseError", Err: err} } @@ -5517,7 +5520,7 @@ func (r *ClickHouseReader) GetAllMetricFilterAttributeValues(ctx context.Context rows, err = r.db.Query(valueCtx, query, req.FilterKey, req.FilterKey, fmt.Sprintf("%%%s%%", req.SearchText), common.PastDayRoundOff(), normalized) //only showing past day data if err != nil { - r.logger.Error("Error while executing query", "error", err) + r.logger.Error("Error while executing query", errorsV2.Attr(err)) return nil, &model.ApiError{Typ: "ClickHouseError", Err: err} } defer rows.Close() @@ -5551,7 +5554,7 @@ func (r *ClickHouseReader) GetAllMetricFilterUnits(ctx context.Context, req *met valueCtx := context.WithValue(ctx, "clickhouse_max_threads", constants.MetricsExplorerClickhouseThreads) rows, err := r.db.Query(valueCtx, query, fmt.Sprintf("%%%s%%", req.SearchText)) if err != nil { - r.logger.Error("Error while executing query", "error", err) + r.logger.Error("Error while executing query", errorsV2.Attr(err)) return nil, &model.ApiError{Typ: "ClickHouseError", Err: err} } @@ -5582,7 +5585,7 @@ func (r *ClickHouseReader) GetAllMetricFilterTypes(ctx context.Context, req *met valueCtx := context.WithValue(ctx, "clickhouse_max_threads", constants.MetricsExplorerClickhouseThreads) rows, err := r.db.Query(valueCtx, query, fmt.Sprintf("%%%s%%", req.SearchText)) if err != nil { - r.logger.Error("Error while executing query", "error", err) + r.logger.Error("Error while executing query", errorsV2.Attr(err)) return nil, &model.ApiError{Typ: "ClickHouseError", Err: err} } @@ -5821,7 +5824,7 @@ func (r *ClickHouseReader) ListSummaryMetrics(ctx context.Context, orgID valuer. queryDuration := time.Since(begin) r.logger.Info("Time taken to execute metrics query to fetch metrics with high time series", "query", metricsQuery, "args", args, "duration", queryDuration) if err != nil { - r.logger.Error("Error executing metrics query", "error", err) + r.logger.Error("Error executing metrics query", errorsV2.Attr(err)) return &metrics_explorer.SummaryListMetricsResponse{}, &model.ApiError{Typ: "ClickHouseError", Err: err} } defer rows.Close() @@ -5832,14 +5835,14 @@ func (r *ClickHouseReader) ListSummaryMetrics(ctx context.Context, orgID valuer. for rows.Next() { var metric metrics_explorer.MetricDetail if err := rows.Scan(&metric.MetricName, &metric.Description, &metric.MetricType, &metric.MetricUnit, &metric.TimeSeries, &response.Total); err != nil { - r.logger.Error("Error scanning metric row", "error", err) + r.logger.Error("Error scanning metric row", errorsV2.Attr(err)) return &response, &model.ApiError{Typ: "ClickHouseError", Err: err} } metricNames = append(metricNames, metric.MetricName) response.Metrics = append(response.Metrics, metric) } if err := rows.Err(); err != nil { - r.logger.Error("Error iterating over metric rows", "error", err) + r.logger.Error("Error iterating over metric rows", errorsV2.Attr(err)) return &response, &model.ApiError{Typ: "ClickHouseError", Err: err} } // If no metrics were found, return early. @@ -5926,7 +5929,7 @@ func (r *ClickHouseReader) ListSummaryMetrics(ctx context.Context, orgID valuer. queryDuration = time.Since(begin) r.logger.Info("Time taken to execute list summary query", "query", sampleQuery, "args", args, "duration", queryDuration) if err != nil { - r.logger.Error("Error executing samples query", "error", err) + r.logger.Error("Error executing samples query", errorsV2.Attr(err)) return &response, &model.ApiError{Typ: "ClickHouseError", Err: err} } defer rows.Close() @@ -5937,20 +5940,20 @@ func (r *ClickHouseReader) ListSummaryMetrics(ctx context.Context, orgID valuer. var samples uint64 var metricName string if err := rows.Scan(&samples, &metricName); err != nil { - r.logger.Error("Error scanning sample row", "error", err) + r.logger.Error("Error scanning sample row", errorsV2.Attr(err)) return &response, &model.ApiError{Typ: "ClickHouseError", Err: err} } samplesMap[metricName] = samples } if err := rows.Err(); err != nil { - r.logger.Error("Error iterating over sample rows", "error", err) + r.logger.Error("Error iterating over sample rows", errorsV2.Attr(err)) return &response, &model.ApiError{Typ: "ClickHouseError", Err: err} } //get updated metrics data batch, apiError := r.GetUpdatedMetricsMetadata(ctx, orgID, metricNames...) if apiError != nil { - r.logger.Error("Error in getting metrics cached metadata", "error", apiError) + r.logger.Error("Error in getting metrics cached metadata", errorsV2.Attr(apiError)) } var filteredMetrics []metrics_explorer.MetricDetail @@ -6042,7 +6045,7 @@ func (r *ClickHouseReader) GetMetricsTimeSeriesPercentage(ctx context.Context, r duration := time.Since(begin) r.logger.Info("Time taken to execute time series percentage query", "query", query, "args", args, "duration", duration) if err != nil { - r.logger.Error("Error executing time series percentage query", "error", err, "query", query) + r.logger.Error("Error executing time series percentage query", errorsV2.Attr(err), "query", query) return nil, &model.ApiError{Typ: "ClickHouseError", Err: err} } defer rows.Close() @@ -6051,14 +6054,14 @@ func (r *ClickHouseReader) GetMetricsTimeSeriesPercentage(ctx context.Context, r for rows.Next() { var item metrics_explorer.TreeMapResponseItem if err := rows.Scan(&item.MetricName, &item.TotalValue, &item.Percentage); err != nil { - r.logger.Error("Error scanning row", "error", err) + r.logger.Error("Error scanning row", errorsV2.Attr(err)) return nil, &model.ApiError{Typ: "ClickHouseError", Err: err} } treemap = append(treemap, item) } if err := rows.Err(); err != nil { - r.logger.Error("Error iterating over rows", "error", err) + r.logger.Error("Error iterating over rows", errorsV2.Attr(err)) return nil, &model.ApiError{Typ: "ClickHouseError", Err: err} } @@ -6109,7 +6112,7 @@ func (r *ClickHouseReader) GetMetricsSamplesPercentage(ctx context.Context, req duration := time.Since(begin) r.logger.Info("Time taken to execute samples percentage metric name query to reduce search space", "query", metricsQuery, "start", start, "end", end, "duration", duration) if err != nil { - r.logger.Error("Error executing samples percentage query", "error", err) + r.logger.Error("Error executing samples percentage query", errorsV2.Attr(err)) return nil, &model.ApiError{Typ: "ClickHouseError", Err: err} } defer rows.Close() @@ -6120,13 +6123,13 @@ func (r *ClickHouseReader) GetMetricsSamplesPercentage(ctx context.Context, req var metricName string var timeSeries uint64 if err := rows.Scan(&metricName, &timeSeries); err != nil { - r.logger.Error("Error scanning metric row", "error", err) + r.logger.Error("Error scanning metric row", errorsV2.Attr(err)) return nil, &model.ApiError{Typ: "ClickHouseError", Err: err} } metricNames = append(metricNames, metricName) } if err := rows.Err(); err != nil { - r.logger.Error("Error iterating over metric rows", "error", err) + r.logger.Error("Error iterating over metric rows", errorsV2.Attr(err)) return nil, &model.ApiError{Typ: "ClickHouseError", Err: err} } @@ -6207,7 +6210,7 @@ func (r *ClickHouseReader) GetMetricsSamplesPercentage(ctx context.Context, req duration = time.Since(begin) r.logger.Info("Time taken to execute samples percentage query", "query", sampleQuery, "args", args, "duration", duration) if err != nil { - r.logger.Error("Error executing samples query", "error", err) + r.logger.Error("Error executing samples query", errorsV2.Attr(err)) return nil, &model.ApiError{Typ: "ClickHouseError", Err: err} } defer rows.Close() @@ -6217,13 +6220,13 @@ func (r *ClickHouseReader) GetMetricsSamplesPercentage(ctx context.Context, req for rows.Next() { var item metrics_explorer.TreeMapResponseItem if err := rows.Scan(&item.TotalValue, &item.MetricName, &item.Percentage); err != nil { - r.logger.Error("Error scanning row", "error", err) + r.logger.Error("Error scanning row", errorsV2.Attr(err)) return nil, &model.ApiError{Typ: "ClickHouseError", Err: err} } treemap = append(treemap, item) } if err := rows.Err(); err != nil { - r.logger.Error("Error iterating over sample rows", "error", err) + r.logger.Error("Error iterating over sample rows", errorsV2.Attr(err)) return nil, &model.ApiError{Typ: "ClickHouseError", Err: err} } @@ -6827,7 +6830,7 @@ func (r *ClickHouseReader) GetUpdatedMetricsMetadata(ctx context.Context, orgID cacheKey := constants.UpdatedMetricsMetadataCachePrefix + metadata.MetricName if cacheErr := r.cache.Set(ctx, orgID, cacheKey, metadata, 0); cacheErr != nil { - r.logger.Error("Failed to store metrics metadata in cache", "metric_name", metadata.MetricName, "error", cacheErr) + r.logger.Error("Failed to store metrics metadata in cache", "metric_name", metadata.MetricName, errorsV2.Attr(cacheErr)) } cachedMetadata[metadata.MetricName] = metadata found[metadata.MetricName] = struct{}{} @@ -6868,7 +6871,7 @@ func (r *ClickHouseReader) GetUpdatedMetricsMetadata(ctx context.Context, orgID cacheKey := constants.UpdatedMetricsMetadataCachePrefix + metadata.MetricName if cacheErr := r.cache.Set(ctx, orgID, cacheKey, metadata, 0); cacheErr != nil { - r.logger.Error("Failed to cache fallback metadata", "metric_name", metadata.MetricName, "error", cacheErr) + r.logger.Error("Failed to cache fallback metadata", "metric_name", metadata.MetricName, errorsV2.Attr(cacheErr)) } cachedMetadata[metadata.MetricName] = metadata } @@ -6900,7 +6903,7 @@ func (r *ClickHouseReader) SearchTraces(ctx context.Context, params *model.Searc if err == sql.ErrNoRows { return &searchSpansResult, nil } - r.logger.Error("Error in processing sql query", "error", err) + r.logger.Error("Error in processing sql query", errorsV2.Attr(err)) return nil, fmt.Errorf("error in processing sql query") } @@ -6915,7 +6918,7 @@ func (r *ClickHouseReader) SearchTraces(ctx context.Context, params *model.Searc query := fmt.Sprintf("SELECT timestamp, duration_nano, span_id, trace_id, has_error, kind, resource_string_service$$name, name, links as references, attributes_string, attributes_number, attributes_bool, resources_string, events, status_message, status_code_string, kind_string FROM %s.%s WHERE trace_id=$1 and ts_bucket_start>=$2 and ts_bucket_start<=$3", r.TraceDB, r.traceTableName) err = r.db.Select(ctx, &searchScanResponses, query, params.TraceID, strconv.FormatInt(traceSummary.Start.Unix()-1800, 10), strconv.FormatInt(traceSummary.End.Unix(), 10)) if err != nil { - r.logger.Error("Error in processing sql query", "error", err) + r.logger.Error("Error in processing sql query", errorsV2.Attr(err)) return nil, fmt.Errorf("error in processing sql query") } @@ -6927,7 +6930,7 @@ func (r *ClickHouseReader) SearchTraces(ctx context.Context, params *model.Searc ref := []model.OtelSpanRef{} err := json.Unmarshal([]byte(item.References), &ref) if err != nil { - r.logger.Error("Error unmarshalling references", "error", err) + r.logger.Error("Error unmarshalling references", errorsV2.Attr(err)) return nil, err } diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 7f4b04a2b54..7ebc0e15b09 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -25,6 +25,8 @@ import ( "text/template" "time" + "github.com/prometheus/prometheus/promql" + "github.com/SigNoz/signoz/pkg/alertmanager" errorsV2 "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/http/middleware" @@ -35,7 +37,6 @@ import ( "github.com/SigNoz/signoz/pkg/query-service/app/metricsexplorer" "github.com/SigNoz/signoz/pkg/signoz" "github.com/SigNoz/signoz/pkg/valuer" - "github.com/prometheus/prometheus/promql" "github.com/gorilla/mux" "github.com/gorilla/websocket" @@ -252,13 +253,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) + aH.logger.Warn("unexpected error while fetching orgs while initializing base api handler", errors.Attr(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) + aH.logger.Warn("unexpected error while fetching user count while initializing base api handler", errors.Attr(err)) } if count > 0 { @@ -313,7 +314,7 @@ func RespondError(w http.ResponseWriter, apiErr model.BaseApiError, data interfa Data: data, }) if err != nil { - slog.Error("error marshalling json response", "error", err) + slog.Error("error marshalling json response", errors.Attr(err)) http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -345,7 +346,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) + slog.Error("error writing response", "bytes_written", n, errors.Attr(err)) } } @@ -357,7 +358,7 @@ func writeHttpResponse(w http.ResponseWriter, data interface{}) { Data: data, }) if err != nil { - slog.Error("error marshalling json response", "error", err) + slog.Error("error marshalling json response", errors.Attr(err)) http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -365,7 +366,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) + slog.Error("error writing response", "bytes_written", n, errors.Attr(err)) } } @@ -937,14 +938,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) + aH.logger.ErrorContext(ctx, "failed to get log fields using empty keys", errors.Attr(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) + aH.logger.ErrorContext(ctx, "failed to get span attributes using empty keys", errors.Attr(err)) } } @@ -1277,14 +1278,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) + aH.logger.ErrorContext(ctx, "failed to get dashboards for installed integrations", errors.Attr(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) + aH.logger.ErrorContext(ctx, "failed to get dashboards for cloud integrations", errors.Attr(apiErr)) } else { dashboards = append(dashboards, cloudIntegrationDashboards...) } @@ -1326,7 +1327,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) + aH.logger.ErrorContext(r.Context(), "error reading request body for test rule", errors.Attr(err)) RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) return } @@ -1378,7 +1379,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) + aH.logger.ErrorContext(r.Context(), "error reading request body for patch rule", errors.Attr(err)) RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) return } @@ -1408,7 +1409,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) + aH.logger.ErrorContext(r.Context(), "error reading request body for edit rule", errors.Attr(err)) RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) return } @@ -1433,7 +1434,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) + aH.logger.ErrorContext(r.Context(), "error reading request body for create rule", errors.Attr(err)) RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) return } @@ -1479,7 +1480,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) + aH.logger.ErrorContext(r.Context(), "error in query range metrics", errors.Attr(res.Err)) } if res.Err != nil { @@ -1534,7 +1535,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) + aH.logger.ErrorContext(r.Context(), "error in query range metrics", errors.Attr(res.Err)) } if res.Err != nil { @@ -1637,7 +1638,7 @@ func (aH *APIHandler) getServicesTopLevelOps(w http.ResponseWriter, r *http.Requ var params topLevelOpsParams err := json.NewDecoder(r.Body).Decode(¶ms) if err != nil { - aH.logger.ErrorContext(r.Context(), "error reading request body for get top operations", "error", err) + aH.logger.ErrorContext(r.Context(), "error reading request body for get top operations", errors.Attr(err)) } if params.Service != "" { @@ -2059,7 +2060,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) + aH.logger.Error("internal server error in http handler", errors.Attr(err)) } structuredResp := structuredResponse{ Errors: []structuredError{ @@ -2153,7 +2154,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) + aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", errors.Attr(apiErr.Err)) RespondError(w, apiErr, nil) return } @@ -2161,7 +2162,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) + aH.logger.ErrorContext(r.Context(), "failed to build clickhouse query for onboard producers", errors.Attr(err)) RespondError(w, apiErr, nil) return } @@ -2255,7 +2256,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) + aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", errors.Attr(apiErr.Err)) RespondError(w, apiErr, nil) return } @@ -2263,7 +2264,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) + aH.logger.ErrorContext(r.Context(), "failed to build clickhouse query for onboard consumers", errors.Attr(err)) RespondError(w, apiErr, nil) return } @@ -2402,7 +2403,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) + aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", errors.Attr(apiErr.Err)) RespondError(w, apiErr, nil) return } @@ -2410,7 +2411,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) + aH.logger.ErrorContext(r.Context(), "failed to build kafka onboarding queries", errors.Attr(err)) RespondError(w, apiErr, nil) return } @@ -2512,19 +2513,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) + aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", errors.Attr(apiErr.Err)) 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) + aH.logger.ErrorContext(r.Context(), "failed to build query range params for throughput", errors.Attr(err)) RespondError(w, apiErr, nil) return } if err := validateQueryRangeParamsV3(queryRangeParams); err != nil { - aH.logger.ErrorContext(r.Context(), "failed to validate query range params", "error", err) + aH.logger.ErrorContext(r.Context(), "failed to validate query range params", errors.Attr(err)) RespondError(w, apiErr, nil) return } @@ -2563,12 +2564,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) + aH.logger.ErrorContext(r.Context(), "failed to build query range params for fetch latency", errors.Attr(err)) 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) + aH.logger.ErrorContext(r.Context(), "failed to validate query range params for fetch latency", errors.Attr(err)) RespondError(w, apiErr, nil) return } @@ -2623,7 +2624,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) + aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", errors.Attr(apiErr.Err)) RespondError(w, apiErr, nil) return } @@ -2637,13 +2638,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) + aH.logger.ErrorContext(r.Context(), "failed to build query range params for producer", errors.Attr(err)) 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) + aH.logger.ErrorContext(r.Context(), "failed to validate query range params for producer", errors.Attr(err)) RespondError(w, apiErr, nil) return } @@ -2680,7 +2681,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) + aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", errors.Attr(apiErr.Err)) RespondError(w, apiErr, nil) return } @@ -2690,13 +2691,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) + aH.logger.ErrorContext(r.Context(), "failed to build query range params for consumer", errors.Attr(err)) 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) + aH.logger.ErrorContext(r.Context(), "failed to validate query range params for consumer", errors.Attr(err)) RespondError(w, apiErr, nil) return } @@ -2738,7 +2739,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) + aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", errors.Attr(apiErr.Err)) RespondError(w, apiErr, nil) return } @@ -2748,13 +2749,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) + aH.logger.ErrorContext(r.Context(), "failed to build query range params for producer topic throughput", errors.Attr(err)) 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) + aH.logger.ErrorContext(r.Context(), "failed to validate query range params for producer topic throughput", errors.Attr(err)) RespondError(w, apiErr, nil) return } @@ -2796,7 +2797,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) + aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", errors.Attr(apiErr.Err)) RespondError(w, apiErr, nil) return } @@ -2806,13 +2807,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) + aH.logger.ErrorContext(r.Context(), "failed to build query range params for consumer partition latency", errors.Attr(err)) 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) + aH.logger.ErrorContext(r.Context(), "failed to validate query range params for consumer partition latency", errors.Attr(err)) RespondError(w, apiErr, nil) return } @@ -2856,7 +2857,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) + aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", errors.Attr(apiErr.Err)) RespondError(w, apiErr, nil) return } @@ -2867,13 +2868,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) + aH.logger.ErrorContext(r.Context(), "failed to build query range params for producer throughput overview", errors.Attr(err)) 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) + aH.logger.ErrorContext(r.Context(), "failed to validate query range params for producer throughput overview", errors.Attr(err)) RespondError(w, apiErr, nil) return } @@ -2909,12 +2910,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) + aH.logger.ErrorContext(r.Context(), "failed to build query range params for producer throughput byte rate", errors.Attr(err)) 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) + aH.logger.ErrorContext(r.Context(), "failed to validate query range params for producer throughput byte rate", errors.Attr(err)) RespondError(w, apiErr, nil) return } @@ -2972,7 +2973,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) + aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", errors.Attr(apiErr.Err)) RespondError(w, apiErr, nil) return } @@ -2982,13 +2983,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) + aH.logger.ErrorContext(r.Context(), "failed to build query range params for producer throughput details", errors.Attr(err)) 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) + aH.logger.ErrorContext(r.Context(), "failed to validate query range params for producer throughput details", errors.Attr(err)) RespondError(w, apiErr, nil) return } @@ -3030,7 +3031,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) + aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", errors.Attr(apiErr.Err)) RespondError(w, apiErr, nil) return } @@ -3040,13 +3041,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) + aH.logger.ErrorContext(r.Context(), "failed to build query range params for consumer throughput overview", errors.Attr(err)) 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) + aH.logger.ErrorContext(r.Context(), "failed to validate query range params for consumer throughput overview", errors.Attr(err)) RespondError(w, apiErr, nil) return } @@ -3088,7 +3089,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) + aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", errors.Attr(apiErr.Err)) RespondError(w, apiErr, nil) return } @@ -3098,13 +3099,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) + aH.logger.ErrorContext(r.Context(), "failed to build query range params for consumer throughput details", errors.Attr(err)) 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) + aH.logger.ErrorContext(r.Context(), "failed to validate query range params for consumer throughput details", errors.Attr(err)) RespondError(w, apiErr, nil) return } @@ -3149,7 +3150,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) + aH.logger.ErrorContext(r.Context(), "failed to parse kafka queue body", errors.Attr(apiErr.Err)) RespondError(w, apiErr, nil) return } @@ -3159,7 +3160,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) + aH.logger.ErrorContext(r.Context(), "failed to build query range params for producer consumer eval", errors.Attr(err)) RespondError(w, &model.ApiError{ Typ: model.ErrorBadData, Err: err, @@ -3168,7 +3169,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) + aH.logger.ErrorContext(r.Context(), "failed to validate query range params for producer consumer eval", errors.Attr(err)) RespondError(w, apiErr, nil) return } @@ -4454,7 +4455,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) + aH.logger.ErrorContext(r.Context(), "error parsing query range params", errors.Attr(apiErrorObj.Err)) RespondError(w, apiErrorObj, nil) return } @@ -4534,7 +4535,7 @@ func (aH *APIHandler) queryRangeV3(ctx context.Context, queryRangeParams *v3.Que if apiErr != nil { aH.logger.ErrorContext(ctx, "failed to report query start for progress tracking", - "query_id", queryIdHeader, "error", apiErr, + "query_id", queryIdHeader, errors.Attr(apiErr), ) } else { @@ -4709,7 +4710,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) + aH.logger.ErrorContext(r.Context(), "error parsing metric query range params", errors.Attr(apiErrorObj.Err)) RespondError(w, apiErrorObj, nil) return } @@ -4717,7 +4718,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) + aH.logger.ErrorContext(r.Context(), "error adding temporality for metrics", errors.Attr(temporalityErr)) RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: temporalityErr}, nil) return } @@ -4762,7 +4763,7 @@ func (aH *APIHandler) GetQueryProgressUpdates(w http.ResponseWriter, r *http.Req 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, + "query_id", queryId, errors.Attr(apiErr), ) return } @@ -4772,7 +4773,7 @@ func (aH *APIHandler) GetQueryProgressUpdates(w http.ResponseWriter, r *http.Req 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, + "query_id", queryId, "progress", queryProgress, errors.Attr(err), ) continue } @@ -4780,7 +4781,7 @@ func (aH *APIHandler) GetQueryProgressUpdates(w http.ResponseWriter, r *http.Req 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, + "query_id", queryId, "msg", string(msg), errors.Attr(err), ) break @@ -4928,7 +4929,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) + aH.logger.ErrorContext(r.Context(), "error parsing metric query range params", errors.Attr(apiErrorObj.Err)) RespondError(w, apiErrorObj, nil) return } @@ -4937,7 +4938,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) + aH.logger.ErrorContext(r.Context(), "error adding temporality for metrics", errors.Attr(temporalityErr)) RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: temporalityErr}, nil) return } @@ -4986,7 +4987,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) + aH.logger.ErrorContext(r.Context(), "failed to parse queue body", errors.Attr(apiErr.Err)) RespondError(w, apiErr, nil) return } @@ -4994,7 +4995,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) + aH.logger.ErrorContext(r.Context(), "failed to build queue overview query", errors.Attr(err)) RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error building clickhouse query: %v", err)}, nil) return } @@ -5025,7 +5026,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) + aH.logger.ErrorContext(r.Context(), "failed to parse request body", errors.Attr(apiErr)) render.Error(w, errorsV2.New(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, apiErr.Error())) return } @@ -5033,7 +5034,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) + aH.logger.ErrorContext(r.Context(), "failed to build domain list query", errors.Attr(err)) apiErrObj := errorsV2.New(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, err.Error()) render.Error(w, apiErrObj) return @@ -5046,7 +5047,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) + aH.logger.ErrorContext(r.Context(), "query execution failed", errors.Attr(err)) apiErrObj := errorsV2.New(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, err.Error()) render.Error(w, apiErrObj) return @@ -5085,7 +5086,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) + aH.logger.ErrorContext(r.Context(), "failed to parse request body", errors.Attr(apiErr)) render.Error(w, errorsV2.New(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, apiErr.Error())) return } @@ -5093,7 +5094,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) + aH.logger.ErrorContext(r.Context(), "failed to build domain info query", errors.Attr(err)) apiErrObj := errorsV2.New(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, err.Error()) render.Error(w, apiErrObj) return @@ -5106,7 +5107,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) + aH.logger.ErrorContext(r.Context(), "query execution failed", errors.Attr(err)) apiErrObj := errorsV2.New(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, err.Error()) render.Error(w, apiErrObj) return diff --git a/pkg/query-service/app/logparsingpipeline/controller.go b/pkg/query-service/app/logparsingpipeline/controller.go index fe2dd1025cd..1e11c507de1 100644 --- a/pkg/query-service/app/logparsingpipeline/controller.go +++ b/pkg/query-service/app/logparsingpipeline/controller.go @@ -7,6 +7,8 @@ import ( "slices" "strings" + "github.com/google/uuid" + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/query-service/agentConf" "github.com/SigNoz/signoz/pkg/query-service/constants" @@ -19,7 +21,6 @@ import ( "github.com/SigNoz/signoz/pkg/types/opamptypes" "github.com/SigNoz/signoz/pkg/types/pipelinetypes" "github.com/SigNoz/signoz/pkg/valuer" - "github.com/google/uuid" "log/slog" ) @@ -175,7 +176,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) + slog.ErrorContext(ctx, "failed to get pipelines for version", "version", version, errors.Attr(err)) return nil, err } result = savedPipelines @@ -227,7 +228,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) + slog.ErrorContext(ctx, "failed to get pipelines for version", "version", version, errors.Attr(err)) return nil, err } @@ -235,7 +236,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) + slog.ErrorContext(ctx, "failed to get config for version", "version", version, errors.Attr(err)) return nil, err } configVersion = cv diff --git a/pkg/query-service/app/logparsingpipeline/db.go b/pkg/query-service/app/logparsingpipeline/db.go index 51bb926b68b..fdcf2b8ba84 100644 --- a/pkg/query-service/app/logparsingpipeline/db.go +++ b/pkg/query-service/app/logparsingpipeline/db.go @@ -81,7 +81,7 @@ func (r *Repo) insertPipeline( Model(&insertRow.StoreablePipeline). Exec(ctx) if err != nil { - slog.ErrorContext(ctx, "error in inserting pipeline data", "error", err) + slog.ErrorContext(ctx, "error in inserting pipeline data", errors.Attr(err)) return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to insert pipeline") } @@ -137,7 +137,7 @@ 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) + slog.ErrorContext(ctx, "failed to get ingestion pipeline from db", errors.Attr(err)) return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to get ingestion pipeline from db") } @@ -150,11 +150,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) + slog.ErrorContext(ctx, "invalid pipeline config found", "id", id, errors.Attr(err)) return nil, err } if err := gettablePipeline.ParseFilter(); err != nil { - slog.ErrorContext(ctx, "invalid pipeline filter found", "id", id, "error", err) + slog.ErrorContext(ctx, "invalid pipeline filter found", "id", id, errors.Attr(err)) return nil, err } return &gettablePipeline, nil diff --git a/pkg/query-service/app/metricsexplorer/summary.go b/pkg/query-service/app/metricsexplorer/summary.go index 582817ed4a3..9a00b30585a 100644 --- a/pkg/query-service/app/metricsexplorer/summary.go +++ b/pkg/query-service/app/metricsexplorer/summary.go @@ -5,11 +5,16 @@ import ( "encoding/json" "errors" "sort" + "strings" "time" + signozerrors "github.com/SigNoz/signoz/pkg/errors" + "log/slog" + "golang.org/x/sync/errgroup" + "github.com/SigNoz/signoz/pkg/modules/dashboard" "github.com/SigNoz/signoz/pkg/query-service/interfaces" "github.com/SigNoz/signoz/pkg/query-service/model" @@ -18,7 +23,6 @@ import ( "github.com/SigNoz/signoz/pkg/query-service/rules" "github.com/SigNoz/signoz/pkg/types/authtypes" "github.com/SigNoz/signoz/pkg/valuer" - "golang.org/x/sync/errgroup" ) type SummaryService struct { @@ -173,14 +177,14 @@ func (receiver *SummaryService) GetMetricsSummary(ctx context.Context, orgID val if data != nil { jsonData, err := json.Marshal(data) if err != nil { - slog.Error("error marshalling data", "error", err) + slog.Error("error marshalling data", signozerrors.Attr(err)) return &model.ApiError{Typ: "MarshallingErr", Err: err} } var dashboards map[string][]metrics_explorer.Dashboard err = json.Unmarshal(jsonData, &dashboards) if err != nil { - slog.Error("error unmarshalling data", "error", err) + slog.Error("error unmarshalling data", signozerrors.Attr(err)) return &model.ApiError{Typ: "UnMarshallingErr", Err: err} } if _, ok := dashboards[metricName]; ok { @@ -350,12 +354,12 @@ func (receiver *SummaryService) GetRelatedMetrics(ctx context.Context, params *m if names != nil { jsonData, err := json.Marshal(names) if err != nil { - slog.Error("error marshalling dashboard data", "error", err) + slog.Error("error marshalling dashboard data", signozerrors.Attr(err)) return &model.ApiError{Typ: "MarshallingErr", Err: err} } err = json.Unmarshal(jsonData, &dashboardsRelatedData) if err != nil { - slog.Error("error unmarshalling dashboard data", "error", err) + slog.Error("error unmarshalling dashboard data", signozerrors.Attr(err)) return &model.ApiError{Typ: "UnMarshallingErr", Err: err} } } diff --git a/pkg/query-service/app/opamp/configure_ingestionRules.go b/pkg/query-service/app/opamp/configure_ingestionRules.go index d588ad18cfe..3db540e3ce7 100644 --- a/pkg/query-service/app/opamp/configure_ingestionRules.go +++ b/pkg/query-service/app/opamp/configure_ingestionRules.go @@ -5,12 +5,13 @@ import ( "crypto/sha256" "log/slog" - "github.com/SigNoz/signoz/pkg/errors" - model "github.com/SigNoz/signoz/pkg/query-service/app/opamp/model" - "github.com/SigNoz/signoz/pkg/query-service/app/opamp/otelconfig" "github.com/knadh/koanf/parsers/yaml" "github.com/open-telemetry/opamp-go/protobufs" "go.opentelemetry.io/collector/confmap" + + "github.com/SigNoz/signoz/pkg/errors" + model "github.com/SigNoz/signoz/pkg/query-service/app/opamp/model" + "github.com/SigNoz/signoz/pkg/query-service/app/opamp/otelconfig" ) var ( @@ -53,7 +54,7 @@ func UpsertControlProcessors(ctx context.Context, signal string, for _, agent := range agents { agenthash, err := addIngestionControlToAgent(agent, signal, processors, false) if err != nil { - slog.Error("failed to push ingestion rules config to agent", "agent_id", agent.AgentID, "error", err) + slog.Error("failed to push ingestion rules config to agent", "agent_id", agent.AgentID, errors.Attr(err)) continue } @@ -82,7 +83,7 @@ func addIngestionControlToAgent(agent *model.Agent, signal string, processors ma // add ingestion control spec err = makeIngestionControlSpec(agentConf, Signal(signal), processors) if err != nil { - slog.Error("failed to prepare ingestion control processors for agent", "agent_id", agent.AgentID, "error", err) + slog.Error("failed to prepare ingestion control processors for agent", "agent_id", agent.AgentID, errors.Attr(err)) return confHash, err } @@ -133,7 +134,7 @@ func makeIngestionControlSpec(agentConf *confmap.Conf, signal Signal, processors // merge tracesPipelinePlan with current pipeline mergedPipeline, err := buildPipeline(signal, currentPipeline) if err != nil { - slog.Error("failed to build pipeline", "signal", string(signal), "error", err) + slog.Error("failed to build pipeline", "signal", string(signal), errors.Attr(err)) return err } diff --git a/pkg/query-service/app/opamp/model/agent.go b/pkg/query-service/app/opamp/model/agent.go index 28cc1dcdebd..2aca4e1755a 100644 --- a/pkg/query-service/app/opamp/model/agent.go +++ b/pkg/query-service/app/opamp/model/agent.go @@ -8,10 +8,12 @@ import ( "sync" "time" + "google.golang.org/protobuf/proto" + + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/types/opamptypes" "github.com/SigNoz/signoz/pkg/valuer" - "google.golang.org/protobuf/proto" "github.com/open-telemetry/opamp-go/protobufs" opampTypes "github.com/open-telemetry/opamp-go/server/types" @@ -84,7 +86,7 @@ func (agent *Agent) KeepOnlyLast50Agents(ctx context.Context) { Limit(50)). Exec(ctx) if err != nil { - agent.logger.Error("failed to delete old agents", "error", err) + agent.logger.Error("failed to delete old agents", errors.Attr(err)) } } @@ -313,7 +315,7 @@ func (agent *Agent) processStatusUpdate( func (agent *Agent) updateRemoteConfig(configProvider AgentConfigProvider) bool { recommendedConfig, confId, err := configProvider.RecommendAgentConfig(agent.OrgID, []byte(agent.Config)) if err != nil { - agent.logger.Error("could not generate config recommendation for agent", "agent_id", agent.AgentID, "error", err) + agent.logger.Error("could not generate config recommendation for agent", "agent_id", agent.AgentID, errors.Attr(err)) return false } diff --git a/pkg/query-service/app/opamp/opamp_server.go b/pkg/query-service/app/opamp/opamp_server.go index 61f53fc9d62..d8d1e08cf09 100644 --- a/pkg/query-service/app/opamp/opamp_server.go +++ b/pkg/query-service/app/opamp/opamp_server.go @@ -6,12 +6,14 @@ import ( "net/http" "time" - "github.com/SigNoz/signoz/pkg/instrumentation" - model "github.com/SigNoz/signoz/pkg/query-service/app/opamp/model" - "github.com/SigNoz/signoz/pkg/valuer" "github.com/open-telemetry/opamp-go/protobufs" "github.com/open-telemetry/opamp-go/server" "github.com/open-telemetry/opamp-go/server/types" + + "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/instrumentation" + model "github.com/SigNoz/signoz/pkg/query-service/app/opamp/model" + "github.com/SigNoz/signoz/pkg/valuer" ) var opAmpServer *Server @@ -72,7 +74,7 @@ func (srv *Server) Start(listener string) error { err := srv.agents.RecommendLatestConfigToAll(srv.agentConfigProvider) if err != nil { srv.logger.Error( - "could not roll out latest config recommendation to connected agents", "error", err, + "could not roll out latest config recommendation to connected agents", errors.Attr(err), ) } }) @@ -115,7 +117,7 @@ func (srv *Server) OnMessage(ctx context.Context, conn types.Connection, msg *pr // agents sends the effective config when we processStatusUpdate. agent, created, err := srv.agents.FindOrCreateAgent(agentID.String(), conn, orgID) if err != nil { - srv.logger.Error("failed to find or create agent", "agent_id", agentID.String(), "error", err) + srv.logger.Error("failed to find or create agent", "agent_id", agentID.String(), errors.Attr(err)) // Return error response according to OpAMP protocol return &protobufs.ServerToAgent{ diff --git a/pkg/query-service/app/parser.go b/pkg/query-service/app/parser.go index 5f077f2b7f1..1c747ac69ed 100644 --- a/pkg/query-service/app/parser.go +++ b/pkg/query-service/app/parser.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "math" "net/http" "sort" @@ -14,12 +15,16 @@ import ( "text/template" "time" + signozerrors "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/types/thirdpartyapitypes" + "log/slog" + "github.com/SigNoz/govaluate" + "github.com/SigNoz/signoz/pkg/query-service/app/integrations/messagingQueues/kafka" queues2 "github.com/SigNoz/signoz/pkg/query-service/app/integrations/messagingQueues/queues" - "log/slog" "github.com/gorilla/mux" promModel "github.com/prometheus/common/model" @@ -741,7 +746,7 @@ func chTransformQuery(query string, variables map[string]interface{}) { transformer := chVariables.NewQueryTransformer(query, varsForTransform) transformedQuery, err := transformer.Transform() if err != nil { - slog.Warn("failed to transform clickhouse query", "query", query, "error", err) + slog.Warn("failed to transform clickhouse query", "query", query, signozerrors.Attr(err)) } slog.Info("transformed clickhouse query", "transformed_query", transformedQuery, "original_query", query) } diff --git a/pkg/query-service/app/querier/v2/helper.go b/pkg/query-service/app/querier/v2/helper.go index d8a88db941d..7db6063e1b5 100644 --- a/pkg/query-service/app/querier/v2/helper.go +++ b/pkg/query-service/app/querier/v2/helper.go @@ -3,10 +3,12 @@ package v2 import ( "context" "fmt" - "github.com/prometheus/prometheus/promql/parser" "strings" "sync" + "github.com/prometheus/prometheus/promql/parser" + + "github.com/SigNoz/signoz/pkg/errors" logsV4 "github.com/SigNoz/signoz/pkg/query-service/app/logs/v4" metricsV3 "github.com/SigNoz/signoz/pkg/query-service/app/metrics/v3" metricsV4 "github.com/SigNoz/signoz/pkg/query-service/app/metrics/v4" @@ -285,7 +287,7 @@ func (q *querier) ValidateMetricNames(ctx context.Context, query *v3.CompositeQu for _, query := range query.PromQueries { expr, err := parser.ParseExpr(query.Query) if err != nil { - q.logger.DebugContext(ctx, "error parsing promql expression", "query", query.Query, "error", err) + q.logger.DebugContext(ctx, "error parsing promql expression", "query", query.Query, errors.Attr(err)) continue } parser.Inspect(expr, func(node parser.Node, path []parser.Node) error { @@ -301,7 +303,7 @@ func (q *querier) ValidateMetricNames(ctx context.Context, query *v3.CompositeQu } metrics, err := q.reader.GetNormalizedStatus(ctx, orgID, metricNames) if err != nil { - q.logger.DebugContext(ctx, "error getting corresponding normalized metrics", "error", err) + q.logger.DebugContext(ctx, "error getting corresponding normalized metrics", errors.Attr(err)) return } for metricName, metricPresent := range metrics { @@ -319,7 +321,7 @@ func (q *querier) ValidateMetricNames(ctx context.Context, query *v3.CompositeQu } metrics, err := q.reader.GetNormalizedStatus(ctx, orgID, metricNames) if err != nil { - q.logger.DebugContext(ctx, "error getting corresponding normalized metrics", "error", err) + q.logger.DebugContext(ctx, "error getting corresponding normalized metrics", errors.Attr(err)) return } for metricName, metricPresent := range metrics { diff --git a/pkg/query-service/app/server.go b/pkg/query-service/app/server.go index 94a3dd5b819..bff516d6264 100644 --- a/pkg/query-service/app/server.go +++ b/pkg/query-service/app/server.go @@ -9,6 +9,7 @@ import ( "slices" "github.com/SigNoz/signoz/pkg/cache/memorycache" + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/queryparser" "github.com/SigNoz/signoz/pkg/ruler/rulestore/sqlrulestore" @@ -16,6 +17,9 @@ import ( "github.com/gorilla/handlers" + "github.com/rs/cors" + "github.com/soheilhy/cmux" + "github.com/SigNoz/signoz/pkg/alertmanager" "github.com/SigNoz/signoz/pkg/cache" "github.com/SigNoz/signoz/pkg/http/middleware" @@ -35,16 +39,16 @@ import ( "github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/telemetrystore" "github.com/SigNoz/signoz/pkg/web" - "github.com/rs/cors" - "github.com/soheilhy/cmux" + + "log/slog" + + "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux" + "go.opentelemetry.io/otel/propagation" "github.com/SigNoz/signoz/pkg/query-service/constants" "github.com/SigNoz/signoz/pkg/query-service/healthcheck" "github.com/SigNoz/signoz/pkg/query-service/rules" "github.com/SigNoz/signoz/pkg/query-service/utils" - "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux" - "go.opentelemetry.io/otel/propagation" - "log/slog" ) // Server runs HTTP, Mux and a grpc server @@ -285,7 +289,7 @@ func (s *Server) Start(ctx context.Context) error { case nil, http.ErrServerClosed, cmux.ErrListenerClosed: // normal exit, nothing to do default: - slog.Error("Could not start HTTP server", "error", err) + slog.Error("Could not start HTTP server", errors.Attr(err)) } s.unavailableChannel <- healthcheck.Unavailable }() @@ -295,7 +299,7 @@ func (s *Server) Start(ctx context.Context) error { err = http.ListenAndServe(constants.DebugHttpPort, nil) if err != nil { - slog.Error("Could not start pprof server", "error", err) + slog.Error("Could not start pprof server", errors.Attr(err)) } }() @@ -303,7 +307,7 @@ func (s *Server) Start(ctx context.Context) error { slog.Info("Starting OpAmp Websocket server", "addr", constants.OpAmpWsEndpoint) err := s.opampServer.Start(constants.OpAmpWsEndpoint) if err != nil { - slog.Error("opamp ws server failed to start", "error", err) + slog.Error("opamp ws server failed to start", errors.Attr(err)) s.unavailableChannel <- healthcheck.Unavailable } }() diff --git a/pkg/query-service/app/summary.go b/pkg/query-service/app/summary.go index af43de54fc0..6d5878097cb 100644 --- a/pkg/query-service/app/summary.go +++ b/pkg/query-service/app/summary.go @@ -5,6 +5,7 @@ import ( "io" "net/http" + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/http/render" "github.com/SigNoz/signoz/pkg/query-service/model" "github.com/SigNoz/signoz/pkg/types/authtypes" @@ -23,13 +24,13 @@ func (aH *APIHandler) FilterKeysSuggestion(w http.ResponseWriter, r *http.Reques r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) params, apiError := explorer.ParseFilterKeySuggestions(r) if apiError != nil { - slog.ErrorContext(ctx, "error parsing summary filter keys request", "error", apiError.Err) + slog.ErrorContext(ctx, "error parsing summary filter keys request", errors.Attr(apiError.Err)) RespondError(w, apiError, nil) return } keys, apiError := aH.SummaryService.FilterKeys(ctx, params) if apiError != nil { - slog.ErrorContext(ctx, "error getting filter keys", "error", apiError.Err) + slog.ErrorContext(ctx, "error getting filter keys", errors.Attr(apiError.Err)) RespondError(w, apiError, nil) return } @@ -53,14 +54,14 @@ func (aH *APIHandler) FilterValuesSuggestion(w http.ResponseWriter, r *http.Requ r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) params, apiError := explorer.ParseFilterValueSuggestions(r) if apiError != nil { - slog.ErrorContext(ctx, "error parsing summary filter values request", "error", apiError.Err) + slog.ErrorContext(ctx, "error parsing summary filter values request", errors.Attr(apiError.Err)) RespondError(w, apiError, nil) return } values, apiError := aH.SummaryService.FilterValues(ctx, orgID, params) if apiError != nil { - slog.ErrorContext(ctx, "error getting filter values", "error", apiError.Err) + slog.ErrorContext(ctx, "error getting filter values", errors.Attr(apiError.Err)) RespondError(w, apiError, nil) return } @@ -83,7 +84,7 @@ func (aH *APIHandler) GetMetricsDetails(w http.ResponseWriter, r *http.Request) metricName := mux.Vars(r)["metric_name"] metricsDetail, apiError := aH.SummaryService.GetMetricsSummary(ctx, orgID, metricName) if apiError != nil { - slog.ErrorContext(ctx, "error getting metrics summary", "error", apiError.Err) + slog.ErrorContext(ctx, "error getting metrics summary", errors.Attr(apiError.Err)) RespondError(w, apiError, nil) return } @@ -107,14 +108,14 @@ func (aH *APIHandler) ListMetrics(w http.ResponseWriter, r *http.Request) { r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) params, apiErr := explorer.ParseSummaryListMetricsParams(r) if apiErr != nil { - slog.ErrorContext(ctx, "error parsing metric list metric summary api request", "error", apiErr.Err) + slog.ErrorContext(ctx, "error parsing metric list metric summary api request", errors.Attr(apiErr.Err)) RespondError(w, model.BadRequest(apiErr), nil) return } slmr, apiErr := aH.SummaryService.ListMetricsWithSummary(ctx, orgID, params) if apiErr != nil { - slog.ErrorContext(ctx, "error in getting list metrics summary", "error", apiErr.Err) + slog.ErrorContext(ctx, "error in getting list metrics summary", errors.Attr(apiErr.Err)) RespondError(w, apiErr, nil) return } @@ -127,13 +128,13 @@ func (aH *APIHandler) GetTreeMap(w http.ResponseWriter, r *http.Request) { ctx := r.Context() params, apiError := explorer.ParseTreeMapMetricsParams(r) if apiError != nil { - slog.ErrorContext(ctx, "error parsing tree map metric params", "error", apiError.Err) + slog.ErrorContext(ctx, "error parsing tree map metric params", errors.Attr(apiError.Err)) RespondError(w, apiError, nil) return } result, apiError := aH.SummaryService.GetMetricsTreemap(ctx, params) if apiError != nil { - slog.ErrorContext(ctx, "error getting tree map data", "error", apiError.Err) + slog.ErrorContext(ctx, "error getting tree map data", errors.Attr(apiError.Err)) RespondError(w, apiError, nil) return } @@ -147,13 +148,13 @@ func (aH *APIHandler) GetRelatedMetrics(w http.ResponseWriter, r *http.Request) ctx := r.Context() params, apiError := explorer.ParseRelatedMetricsParams(r) if apiError != nil { - slog.ErrorContext(ctx, "error parsing related metric params", "error", apiError.Err) + slog.ErrorContext(ctx, "error parsing related metric params", errors.Attr(apiError.Err)) RespondError(w, apiError, nil) return } result, apiError := aH.SummaryService.GetRelatedMetrics(ctx, params) if apiError != nil { - slog.ErrorContext(ctx, "error getting related metrics", "error", apiError.Err) + slog.ErrorContext(ctx, "error getting related metrics", errors.Attr(apiError.Err)) RespondError(w, apiError, nil) return } @@ -167,13 +168,13 @@ func (aH *APIHandler) GetInspectMetricsData(w http.ResponseWriter, r *http.Reque ctx := r.Context() params, apiError := explorer.ParseInspectMetricsParams(r) if apiError != nil { - slog.ErrorContext(ctx, "error parsing inspect metric params", "error", apiError.Err) + slog.ErrorContext(ctx, "error parsing inspect metric params", errors.Attr(apiError.Err)) RespondError(w, apiError, nil) return } result, apiError := aH.SummaryService.GetInspectMetrics(ctx, params) if apiError != nil { - slog.ErrorContext(ctx, "error getting inspect metrics data", "error", apiError.Err) + slog.ErrorContext(ctx, "error getting inspect metrics data", errors.Attr(apiError.Err)) RespondError(w, apiError, nil) return } @@ -198,13 +199,13 @@ func (aH *APIHandler) UpdateMetricsMetadata(w http.ResponseWriter, r *http.Reque r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) params, apiError := explorer.ParseUpdateMetricsMetadataParams(r) if apiError != nil { - slog.ErrorContext(ctx, "error parsing update metrics metadata params", "error", apiError.Err) + slog.ErrorContext(ctx, "error parsing update metrics metadata params", errors.Attr(apiError.Err)) RespondError(w, apiError, nil) return } apiError = aH.SummaryService.UpdateMetricsMetadata(ctx, orgID, params) if apiError != nil { - slog.ErrorContext(ctx, "error updating metrics metadata", "error", apiError.Err) + slog.ErrorContext(ctx, "error updating metrics metadata", errors.Attr(apiError.Err)) RespondError(w, apiError, nil) return } diff --git a/pkg/query-service/app/traces/smart/trace.go b/pkg/query-service/app/traces/smart/trace.go index 4ba0e8d95e5..2f83519f407 100644 --- a/pkg/query-service/app/traces/smart/trace.go +++ b/pkg/query-service/app/traces/smart/trace.go @@ -5,6 +5,7 @@ import ( "log/slog" "strconv" + signozerrors "github.com/SigNoz/signoz/pkg/errors" basemodel "github.com/SigNoz/signoz/pkg/query-service/model" ) @@ -53,7 +54,7 @@ func SmartTraceAlgorithm(payload []basemodel.SearchSpanResponseItem, targetSpanI break } if err != nil { - slog.Error("error during breadth first search", "error", err) + slog.Error("error during breadth first search", signozerrors.Attr(err)) return nil, err } } diff --git a/pkg/query-service/app/traces/v3/utils.go b/pkg/query-service/app/traces/v3/utils.go index 033951fa2d2..9878973761e 100644 --- a/pkg/query-service/app/traces/v3/utils.go +++ b/pkg/query-service/app/traces/v3/utils.go @@ -4,6 +4,7 @@ import ( "log/slog" "strconv" + "github.com/SigNoz/signoz/pkg/errors" v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3" "github.com/SigNoz/signoz/pkg/query-service/utils" ) @@ -68,7 +69,7 @@ func TraceIdFilterUsedWithEqual(params *v3.QueryRangeParamsV3) (bool, []string) val := item.Value val, err = utils.ValidateAndCastValue(val, item.Key.DataType) if err != nil { - slog.Error("invalid value for key", "key", item.Key.Key, "error", err) + slog.Error("invalid value for key", "key", item.Key.Key, errors.Attr(err)) return false, []string{} } if val != nil { diff --git a/pkg/query-service/model/v3/v3.go b/pkg/query-service/model/v3/v3.go index 2cba401ade7..d1fbae2f057 100644 --- a/pkg/query-service/model/v3/v3.go +++ b/pkg/query-service/model/v3/v3.go @@ -11,9 +11,11 @@ import ( "log/slog" - "github.com/SigNoz/signoz/pkg/valuer" "github.com/pkg/errors" + signozerrors "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/valuer" + qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" ) @@ -938,7 +940,7 @@ func (b *BuilderQuery) SetShiftByFromFunc() { } else if shift, ok := function.Args[0].(string); ok { shiftBy, err := strconv.ParseFloat(shift, 64) if err != nil { - slog.Error("failed to parse time shift by", "shift", shift, "error", err) + slog.Error("failed to parse time shift by", "shift", shift, signozerrors.Attr(err)) } timeShiftBy = int64(shiftBy) } diff --git a/pkg/query-service/postprocess/process.go b/pkg/query-service/postprocess/process.go index 1990c5d29bd..71c11192981 100644 --- a/pkg/query-service/postprocess/process.go +++ b/pkg/query-service/postprocess/process.go @@ -4,6 +4,8 @@ import ( "log/slog" "github.com/SigNoz/govaluate" + + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/query-service/app/queryBuilder" v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3" ) @@ -56,12 +58,12 @@ func PostProcessResult(result []*v3.Result, queryRangeParams *v3.QueryRangeParam expression, err := govaluate.NewEvaluableExpressionWithFunctions(query.Expression, EvalFuncs()) // This shouldn't happen here, because it should have been caught earlier in validation if err != nil { - slog.Error("error in expression", "error", err) + slog.Error("error in expression", errors.Attr(err)) return nil, err } formulaResult, err := processResults(result, expression, canDefaultZero) if err != nil { - slog.Error("error in expression", "error", err) + slog.Error("error in expression", errors.Attr(err)) return nil, err } formulaResult.QueryName = query.QueryName diff --git a/pkg/query-service/querycache/query_range_cache.go b/pkg/query-service/querycache/query_range_cache.go index e130961f323..d8654a9a8c1 100644 --- a/pkg/query-service/querycache/query_range_cache.go +++ b/pkg/query-service/querycache/query_range_cache.go @@ -7,10 +7,11 @@ import ( "sort" "time" + "log/slog" + "github.com/SigNoz/signoz/pkg/cache" "github.com/SigNoz/signoz/pkg/errors" v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3" - "log/slog" "github.com/SigNoz/signoz/pkg/query-service/utils/labels" "github.com/SigNoz/signoz/pkg/types/cachetypes" @@ -292,7 +293,7 @@ func (q *queryCache) storeMergedData(orgID valuer.UUID, cacheKey string, mergedD cacheableSeriesData := CacheableSeriesData{Series: mergedData} err := q.cache.Set(context.TODO(), orgID, cacheKey, &cacheableSeriesData, 0) if err != nil { - slog.Error("error storing merged data", "error", err) + slog.Error("error storing merged data", errors.Attr(err)) } } diff --git a/pkg/query-service/rules/base_rule.go b/pkg/query-service/rules/base_rule.go index fe1848f25e6..0a20903d609 100644 --- a/pkg/query-service/rules/base_rule.go +++ b/pkg/query-service/rules/base_rule.go @@ -369,7 +369,7 @@ func (r *BaseRule) SendAlerts(ctx context.Context, ts time.Time, resendDelay tim Limit(1). Scan(ctx, &orgID) if err != nil { - r.logger.ErrorContext(ctx, "failed to get org ids", "error", err) + r.logger.ErrorContext(ctx, "failed to get org ids", errors.Attr(err)) return } @@ -485,7 +485,7 @@ func (r *BaseRule) RecordRuleStateHistory(ctx context.Context, prevState, curren } err := r.reader.AddRuleStateHistory(ctx, entries) if err != nil { - r.logger.ErrorContext(ctx, "error while inserting rule state history", "error", err, "itemsToAdd", itemsToAdd) + r.logger.ErrorContext(ctx, "error while inserting rule state history", errors.Attr(err), "itemsToAdd", itemsToAdd) } } r.handledRestart = true diff --git a/pkg/query-service/rules/manager.go b/pkg/query-service/rules/manager.go index 69a7ec9f213..aa8735647c2 100644 --- a/pkg/query-service/rules/manager.go +++ b/pkg/query-service/rules/manager.go @@ -240,7 +240,7 @@ func NewManager(o *ManagerOptions) (*Manager, error) { func (m *Manager) Start(ctx context.Context) { if err := m.initiate(ctx); err != nil { - m.logger.ErrorContext(ctx, "failed to initialize alerting rules manager", "error", err) + m.logger.ErrorContext(ctx, "failed to initialize alerting rules manager", errors.Attr(err)) } m.run(ctx) } @@ -298,7 +298,7 @@ func (m *Manager) initiate(ctx context.Context) error { if !parsedRule.Disabled { err := m.addTask(ctx, org.ID, &parsedRule, taskName) if err != nil { - m.logger.ErrorContext(ctx, "failed to load the rule definition", "name", taskName, "error", err) + m.logger.ErrorContext(ctx, "failed to load the rule definition", "name", taskName, errors.Attr(err)) } } } @@ -419,7 +419,7 @@ func (m *Manager) editTask(_ context.Context, orgID valuer.UUID, rule *ruletypes }) if err != nil { - m.logger.Error("loading tasks failed", "error", err) + m.logger.Error("loading tasks failed", errors.Attr(err)) return errors.NewInvalidInputf(errors.CodeInvalidInput, "error preparing rule with given parameters, previous rule set restored") } @@ -455,7 +455,7 @@ func (m *Manager) editTask(_ context.Context, orgID valuer.UUID, rule *ruletypes func (m *Manager) DeleteRule(ctx context.Context, idStr string) error { id, err := valuer.NewUUID(idStr) if err != nil { - m.logger.Error("delete rule received a rule id in invalid format, must be a valid uuid-v7", "id", idStr, "error", err) + m.logger.Error("delete rule received a rule id in invalid format, must be a valid uuid-v7", "id", idStr, errors.Attr(err)) return fmt.Errorf("delete rule received an rule id in invalid format, must be a valid uuid-v7") } @@ -628,7 +628,7 @@ func (m *Manager) addTask(_ context.Context, orgID valuer.UUID, rule *ruletypes. }) if err != nil { - m.logger.Error("creating rule task failed", "name", taskName, "error", err) + m.logger.Error("creating rule task failed", "name", taskName, errors.Attr(err)) return errors.NewInvalidInputf(errors.CodeInvalidInput, "error loading rules, previous rule set restored") } @@ -784,7 +784,7 @@ func (m *Manager) prepareTestNotifyFunc() NotifyFunc { } err := m.alertmanager.TestAlert(ctx, orgID, ruleID, receiverMap) if err != nil { - m.logger.ErrorContext(ctx, "failed to send test notification", "error", err) + m.logger.ErrorContext(ctx, "failed to send test notification", errors.Attr(err)) return } } @@ -819,7 +819,7 @@ func (m *Manager) ListRuleStates(ctx context.Context) (*ruletypes.GettableRules, ruleResponse := ruletypes.GettableRule{} err = json.Unmarshal([]byte(s.Data), &ruleResponse) if err != nil { - m.logger.ErrorContext(ctx, "failed to unmarshal rule from db", "id", s.ID.StringValue(), "error", err) + m.logger.ErrorContext(ctx, "failed to unmarshal rule from db", "id", s.ID.StringValue(), errors.Attr(err)) continue } @@ -850,7 +850,7 @@ func (m *Manager) GetRule(ctx context.Context, id valuer.UUID) (*ruletypes.Getta r := ruletypes.GettableRule{} err = json.Unmarshal([]byte(s.Data), &r) if err != nil { - m.logger.ErrorContext(ctx, "failed to unmarshal rule from db", "id", s.ID.StringValue(), "error", err) + m.logger.ErrorContext(ctx, "failed to unmarshal rule from db", "id", s.ID.StringValue(), errors.Attr(err)) return nil, err } r.Id = id.StringValue() @@ -919,30 +919,30 @@ func (m *Manager) PatchRule(ctx context.Context, ruleStr string, id valuer.UUID) // retrieve rule from DB storedJSON, err := m.ruleStore.GetStoredRule(ctx, id) if err != nil { - m.logger.ErrorContext(ctx, "failed to get stored rule with given id", "id", id.StringValue(), "error", err) + m.logger.ErrorContext(ctx, "failed to get stored rule with given id", "id", id.StringValue(), errors.Attr(err)) return nil, err } storedRule := ruletypes.PostableRule{} if err := json.Unmarshal([]byte(storedJSON.Data), &storedRule); err != nil { - m.logger.ErrorContext(ctx, "failed to unmarshal rule from db", "id", id.StringValue(), "error", err) + m.logger.ErrorContext(ctx, "failed to unmarshal rule from db", "id", id.StringValue(), errors.Attr(err)) return nil, err } if err := json.Unmarshal([]byte(ruleStr), &storedRule); err != nil { - m.logger.ErrorContext(ctx, "failed to unmarshal patched rule with given id", "id", id.StringValue(), "error", err) + m.logger.ErrorContext(ctx, "failed to unmarshal patched rule with given id", "id", id.StringValue(), errors.Attr(err)) return nil, err } // deploy or un-deploy task according to patched (new) rule state if err := m.syncRuleStateWithTask(ctx, orgID, taskName, &storedRule); err != nil { - m.logger.ErrorContext(ctx, "failed to sync stored rule state with the task", "task_name", taskName, "error", err) + m.logger.ErrorContext(ctx, "failed to sync stored rule state with the task", "task_name", taskName, errors.Attr(err)) return nil, err } newStoredJson, err := json.Marshal(&storedRule) if err != nil { - m.logger.ErrorContext(ctx, "failed to marshal new stored rule with given id", "id", id.StringValue(), "error", err) + m.logger.ErrorContext(ctx, "failed to marshal new stored rule with given id", "id", id.StringValue(), errors.Attr(err)) return nil, err } @@ -954,7 +954,7 @@ func (m *Manager) PatchRule(ctx context.Context, ruleStr string, id valuer.UUID) err = m.ruleStore.EditRule(ctx, storedJSON, func(ctx context.Context) error { return nil }) if err != nil { if err := m.syncRuleStateWithTask(ctx, orgID, taskName, &storedRule); err != nil { - m.logger.ErrorContext(ctx, "failed to restore rule after patch failure", "task_name", taskName, "error", err) + m.logger.ErrorContext(ctx, "failed to restore rule after patch failure", "task_name", taskName, errors.Attr(err)) } return nil, err } @@ -1022,7 +1022,7 @@ func (m *Manager) GetAlertDetailsForMetricNames(ctx context.Context, metricNames result := make(map[string][]ruletypes.GettableRule) rules, err := m.ruleStore.GetStoredRules(ctx, claims.OrgID) if err != nil { - m.logger.ErrorContext(ctx, "error getting stored rules", "error", err) + m.logger.ErrorContext(ctx, "error getting stored rules", errors.Attr(err)) return nil, &model.ApiError{Typ: model.ErrorExec, Err: err} } @@ -1032,7 +1032,7 @@ func (m *Manager) GetAlertDetailsForMetricNames(ctx context.Context, metricNames var rule ruletypes.GettableRule err = json.Unmarshal([]byte(storedRule.Data), &rule) if err != nil { - m.logger.ErrorContext(ctx, "failed to unmarshal rule from db", "id", storedRule.ID.StringValue(), "error", err) + m.logger.ErrorContext(ctx, "failed to unmarshal rule from db", "id", storedRule.ID.StringValue(), errors.Attr(err)) continue } diff --git a/pkg/query-service/rules/prom_rule.go b/pkg/query-service/rules/prom_rule.go index 75b49390df2..8eabe33f1d8 100644 --- a/pkg/query-service/rules/prom_rule.go +++ b/pkg/query-service/rules/prom_rule.go @@ -7,6 +7,9 @@ import ( "log/slog" "time" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/promql" + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/prometheus" "github.com/SigNoz/signoz/pkg/query-service/interfaces" @@ -19,8 +22,6 @@ import ( "github.com/SigNoz/signoz/pkg/types/ruletypes" "github.com/SigNoz/signoz/pkg/units" "github.com/SigNoz/signoz/pkg/valuer" - "github.com/prometheus/prometheus/model/labels" - "github.com/prometheus/prometheus/promql" ) type PromRule struct { @@ -156,7 +157,7 @@ func (r *PromRule) buildAndRunQuery(ctx context.Context, ts time.Time) (ruletype filteredSeries, filterErr := r.BaseRule.FilterNewSeries(ctx, ts, matrixToProcess) // In case of error we log the error and continue with the original series if filterErr != nil { - r.logger.ErrorContext(ctx, "Error filtering new series, ", "error", filterErr, "rule_name", r.Name()) + r.logger.ErrorContext(ctx, "Error filtering new series, ", errors.Attr(filterErr), "rule_name", r.Name()) } else { matrixToProcess = filteredSeries } @@ -233,7 +234,7 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time) (int, error) { result, err := tmpl.Expand() if err != nil { result = fmt.Sprintf("", err) - r.logger.WarnContext(ctx, "Expanding alert template failed", "rule_name", r.Name(), "error", err, "data", tmplData) + r.logger.WarnContext(ctx, "Expanding alert template failed", "rule_name", r.Name(), errors.Attr(err), "data", tmplData) } return result } @@ -311,7 +312,7 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time) (int, error) { for fp, a := range r.Active { labelsJSON, err := json.Marshal(a.QueryResultLables) if err != nil { - r.logger.ErrorContext(ctx, "error marshaling labels", "error", err, "rule_name", r.Name()) + r.logger.ErrorContext(ctx, "error marshaling labels", errors.Attr(err), "rule_name", r.Name()) } if _, ok := resultFPs[fp]; !ok { // If the alert was previously firing, keep it around for a given diff --git a/pkg/query-service/rules/prom_rule_task.go b/pkg/query-service/rules/prom_rule_task.go index 228afcfc3aa..c532f567adb 100644 --- a/pkg/query-service/rules/prom_rule_task.go +++ b/pkg/query-service/rules/prom_rule_task.go @@ -9,12 +9,14 @@ import ( "log/slog" + opentracing "github.com/opentracing/opentracing-go" + plabels "github.com/prometheus/prometheus/model/labels" + + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/types/authtypes" "github.com/SigNoz/signoz/pkg/types/ctxtypes" ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes" "github.com/SigNoz/signoz/pkg/valuer" - opentracing "github.com/opentracing/opentracing-go" - plabels "github.com/prometheus/prometheus/model/labels" ) // PromRuleTask is a promql rule executor @@ -331,7 +333,7 @@ func (g *PromRuleTask) Eval(ctx context.Context, ts time.Time) { g.logger.InfoContext(ctx, "promql rule task", "name", g.name, "eval_started_at", ts) maintenance, err := g.maintenanceStore.GetAllPlannedMaintenance(ctx, g.orgID.StringValue()) if err != nil { - g.logger.ErrorContext(ctx, "error in processing sql query", "error", err) + g.logger.ErrorContext(ctx, "error in processing sql query", errors.Attr(err)) } for i, rule := range g.rules { @@ -381,7 +383,7 @@ func (g *PromRuleTask) Eval(ctx context.Context, ts time.Time) { rule.SetHealth(ruletypes.HealthBad) rule.SetLastError(err) - g.logger.WarnContext(ctx, "evaluating rule failed", "rule_id", rule.ID(), "error", err) + g.logger.WarnContext(ctx, "evaluating rule failed", "rule_id", rule.ID(), errors.Attr(err)) // Canceled queries are intentional termination of queries. This normally // happens on shutdown and thus we skip logging of any errors here. diff --git a/pkg/query-service/rules/rule_task.go b/pkg/query-service/rules/rule_task.go index 140ae260d47..cae076c04e5 100644 --- a/pkg/query-service/rules/rule_task.go +++ b/pkg/query-service/rules/rule_task.go @@ -9,12 +9,14 @@ import ( "log/slog" + opentracing "github.com/opentracing/opentracing-go" + + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/query-service/utils/labels" "github.com/SigNoz/signoz/pkg/types/authtypes" "github.com/SigNoz/signoz/pkg/types/ctxtypes" ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes" "github.com/SigNoz/signoz/pkg/valuer" - opentracing "github.com/opentracing/opentracing-go" ) // RuleTask holds a rule (with composite queries) @@ -317,7 +319,7 @@ func (g *RuleTask) Eval(ctx context.Context, ts time.Time) { maintenance, err := g.maintenanceStore.GetAllPlannedMaintenance(ctx, g.orgID.StringValue()) if err != nil { - g.logger.ErrorContext(ctx, "error in processing sql query", "error", err) + g.logger.ErrorContext(ctx, "error in processing sql query", errors.Attr(err)) } for i, rule := range g.rules { @@ -367,7 +369,7 @@ func (g *RuleTask) Eval(ctx context.Context, ts time.Time) { rule.SetHealth(ruletypes.HealthBad) rule.SetLastError(err) - g.logger.WarnContext(ctx, "evaluating rule failed", "rule_id", rule.ID(), "error", err) + g.logger.WarnContext(ctx, "evaluating rule failed", "rule_id", rule.ID(), errors.Attr(err)) // Canceled queries are intentional termination of queries. This normally // happens on shutdown and thus we skip logging of any errors here. diff --git a/pkg/query-service/rules/test_notification.go b/pkg/query-service/rules/test_notification.go index ee3a485bd48..4cbe7cb9f96 100644 --- a/pkg/query-service/rules/test_notification.go +++ b/pkg/query-service/rules/test_notification.go @@ -5,11 +5,14 @@ import ( "fmt" "time" + "log/slog" + + "github.com/google/uuid" + + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/query-service/model" "github.com/SigNoz/signoz/pkg/query-service/utils/labels" ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes" - "github.com/google/uuid" - "log/slog" ) // TestNotification prepares a dummy rule for given rule parameters and @@ -57,7 +60,7 @@ func defaultTestNotification(opts PrepareTestRuleOptions) (int, *model.ApiError) ) if err != nil { - slog.Error("failed to prepare a new threshold rule for test", "error", err) + slog.Error("failed to prepare a new threshold rule for test", errors.Attr(err)) return 0, model.BadRequest(err) } @@ -79,7 +82,7 @@ func defaultTestNotification(opts PrepareTestRuleOptions) (int, *model.ApiError) ) if err != nil { - slog.Error("failed to prepare a new promql rule for test", "error", err) + slog.Error("failed to prepare a new promql rule for test", errors.Attr(err)) return 0, model.BadRequest(err) } } else { @@ -91,7 +94,7 @@ func defaultTestNotification(opts PrepareTestRuleOptions) (int, *model.ApiError) alertsFound, err := rule.Eval(ctx, ts) if err != nil { - slog.Error("evaluating rule failed", "rule", rule.Name(), "error", err) + slog.Error("evaluating rule failed", "rule", rule.Name(), errors.Attr(err)) return 0, model.InternalError(fmt.Errorf("rule evaluation failed")) } rule.SendAlerts(ctx, ts, 0, time.Duration(1*time.Minute), opts.NotifyFunc) diff --git a/pkg/query-service/rules/threshold_rule.go b/pkg/query-service/rules/threshold_rule.go index 764b24c57ba..79653f00cdd 100644 --- a/pkg/query-service/rules/threshold_rule.go +++ b/pkg/query-service/rules/threshold_rule.go @@ -13,6 +13,7 @@ import ( "time" "github.com/SigNoz/signoz/pkg/contextlinks" + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/query-service/common" "github.com/SigNoz/signoz/pkg/query-service/model" "github.com/SigNoz/signoz/pkg/query-service/postprocess" @@ -446,14 +447,14 @@ func (r *ThresholdRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID, } if err != nil { - r.logger.ErrorContext(ctx, "failed to get alert query range result", "rule_name", r.Name(), "error", err, "query_errors", queryErrors) + r.logger.ErrorContext(ctx, "failed to get alert query range result", "rule_name", r.Name(), errors.Attr(err), "query_errors", queryErrors) return nil, fmt.Errorf("internal error while querying") } if params.CompositeQuery.QueryType == v3.QueryTypeBuilder { results, err = postprocess.PostProcessResult(results, params) if err != nil { - r.logger.ErrorContext(ctx, "failed to post process result", "rule_name", r.Name(), "error", err) + r.logger.ErrorContext(ctx, "failed to post process result", "rule_name", r.Name(), errors.Attr(err)) return nil, fmt.Errorf("internal error while post processing") } } @@ -513,7 +514,7 @@ func (r *ThresholdRule) buildAndRunQueryV5(ctx context.Context, orgID valuer.UUI v5Result, err := r.querierV5.QueryRange(ctx, orgID, params) if err != nil { - r.logger.ErrorContext(ctx, "failed to get alert query result", "rule_name", r.Name(), "error", err) + r.logger.ErrorContext(ctx, "failed to get alert query result", "rule_name", r.Name(), errors.Attr(err)) return nil, fmt.Errorf("internal error while querying") } @@ -554,7 +555,7 @@ func (r *ThresholdRule) buildAndRunQueryV5(ctx context.Context, orgID valuer.UUI filteredSeries, filterErr := r.BaseRule.FilterNewSeries(ctx, ts, seriesToProcess) // In case of error we log the error and continue with the original series if filterErr != nil { - r.logger.ErrorContext(ctx, "Error filtering new series, ", "error", filterErr, "rule_name", r.Name()) + r.logger.ErrorContext(ctx, "Error filtering new series, ", errors.Attr(filterErr), "rule_name", r.Name()) } else { seriesToProcess = filteredSeries } @@ -639,7 +640,7 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time) (int, error) { result, err := tmpl.Expand() if err != nil { result = fmt.Sprintf("", err) - r.logger.ErrorContext(ctx, "Expanding alert template failed", "error", err, "data", tmplData) + r.logger.ErrorContext(ctx, "Expanding alert template failed", errors.Attr(err), "data", tmplData) } return result } @@ -732,7 +733,7 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time) (int, error) { for fp, a := range r.Active { labelsJSON, err := json.Marshal(a.QueryResultLables) if err != nil { - r.logger.ErrorContext(ctx, "error marshaling labels", "error", err, "labels", a.Labels) + r.logger.ErrorContext(ctx, "error marshaling labels", errors.Attr(err), "labels", a.Labels) } if _, ok := resultFPs[fp]; !ok { // If the alert was previously firing, keep it around for a given diff --git a/pkg/querybuilder/where_clause_visitor.go b/pkg/querybuilder/where_clause_visitor.go index 855f3b37dfc..a720248cdfe 100644 --- a/pkg/querybuilder/where_clause_visitor.go +++ b/pkg/querybuilder/where_clause_visitor.go @@ -934,7 +934,7 @@ func (v *filterExpressionVisitor) VisitKey(ctx *grammar.KeyContext) any { v.warnings = append(v.warnings, warnMsg) } v.keysWithWarnings[keyName] = true - v.logger.Warn("ambiguous key", "field_key_name", fieldKey.Name) //nolint:sloglint + v.logger.Warn("ambiguous key", slog.String("field_key_name", fieldKey.Name)) //nolint:sloglint } return fieldKeysForName diff --git a/pkg/queryparser/api.go b/pkg/queryparser/api.go index 6d9e738775f..5be2ec49181 100644 --- a/pkg/queryparser/api.go +++ b/pkg/queryparser/api.go @@ -3,6 +3,7 @@ package queryparser import ( "net/http" + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/http/binding" "github.com/SigNoz/signoz/pkg/http/render" @@ -31,7 +32,7 @@ func (a *API) AnalyzeQueryFilter(w http.ResponseWriter, r *http.Request) { result, err := a.queryParser.AnalyzeQueryFilter(r.Context(), req.QueryType, req.Query) if err != nil { - a.settings.Logger.ErrorContext(r.Context(), "failed to analyze query filter", "error", err) + a.settings.Logger.ErrorContext(r.Context(), "failed to analyze query filter", errors.Attr(err)) render.Error(w, err) return } diff --git a/pkg/ruler/rulestore/sqlrulestore/rule.go b/pkg/ruler/rulestore/sqlrulestore/rule.go index a40215cb28e..8aa99bffeb5 100644 --- a/pkg/ruler/rulestore/sqlrulestore/rule.go +++ b/pkg/ruler/rulestore/sqlrulestore/rule.go @@ -133,7 +133,7 @@ func (r *rule) GetStoredRulesByMetricName(ctx context.Context, orgID string, met for _, storedRule := range storedRules { var ruleData ruletypes.PostableRule if err := json.Unmarshal([]byte(storedRule.Data), &ruleData); err != nil { - r.logger.WarnContext(ctx, "failed to unmarshal rule data", "rule_id", storedRule.ID.StringValue(), "error", err) + r.logger.WarnContext(ctx, "failed to unmarshal rule data", slog.String("rule_id", storedRule.ID.StringValue()), errors.Attr(err)) continue } @@ -167,7 +167,7 @@ func (r *rule) GetStoredRulesByMetricName(ctx context.Context, orgID string, met if spec, ok := queryEnvelope.Spec.(qbtypes.PromQuery); ok { result, err := r.queryParser.AnalyzeQueryFilter(ctx, qbtypes.QueryTypePromQL, spec.Query) if err != nil { - r.logger.WarnContext(ctx, "failed to parse PromQL query", "query", spec.Query, "error", err) + r.logger.WarnContext(ctx, "failed to parse PromQL query", slog.String("query", spec.Query), errors.Attr(err)) continue } if slices.Contains(result.MetricNames, metricName) { @@ -179,7 +179,7 @@ func (r *rule) GetStoredRulesByMetricName(ctx context.Context, orgID string, met if spec, ok := queryEnvelope.Spec.(qbtypes.ClickHouseQuery); ok { result, err := r.queryParser.AnalyzeQueryFilter(ctx, qbtypes.QueryTypeClickHouseSQL, spec.Query) if err != nil { - r.logger.WarnContext(ctx, "failed to parse ClickHouse query", "query", spec.Query, "error", err) + r.logger.WarnContext(ctx, "failed to parse ClickHouse query", slog.String("query", spec.Query), errors.Attr(err)) continue } if slices.Contains(result.MetricNames, metricName) { diff --git a/pkg/signoz/signoz.go b/pkg/signoz/signoz.go index 82f1f659bd8..cf51b108e9e 100644 --- a/pkg/signoz/signoz.go +++ b/pkg/signoz/signoz.go @@ -2,6 +2,7 @@ package signoz import ( "context" + "log/slog" "github.com/SigNoz/signoz/pkg/alertmanager" "github.com/SigNoz/signoz/pkg/alertmanager/nfmanager" @@ -100,8 +101,8 @@ func New( return nil, err } - instrumentation.Logger().InfoContext(ctx, "starting signoz", "version", version.Info.Version(), "variant", version.Info.Variant(), "commit", version.Info.Hash(), "branch", version.Info.Branch(), "go", version.Info.GoVersion(), "time", version.Info.Time()) - instrumentation.Logger().DebugContext(ctx, "loaded signoz config", "config", config) + instrumentation.Logger().InfoContext(ctx, "starting signoz", slog.String("version", version.Info.Version()), slog.String("variant", version.Info.Variant()), slog.String("commit", version.Info.Hash()), slog.String("branch", version.Info.Branch()), slog.String("go", version.Info.GoVersion()), slog.String("time", version.Info.Time())) + instrumentation.Logger().DebugContext(ctx, "loaded signoz config", slog.Any("config", config)) // Get the provider settings from instrumentation providerSettings := instrumentation.ToProviderSettings() diff --git a/pkg/smtp/client/smtp.go b/pkg/smtp/client/smtp.go index faff35e623b..cec5ff983cd 100644 --- a/pkg/smtp/client/smtp.go +++ b/pkg/smtp/client/smtp.go @@ -109,7 +109,7 @@ func (c *Client) Do(ctx context.Context, tos []*mail.Address, subject string, co // Try to clean up after ourselves but don't log anything if something has failed. defer func() { if err := smtpClient.Quit(); success && err != nil { - c.logger.WarnContext(ctx, "failed to close SMTP connection", "error", err) + c.logger.WarnContext(ctx, "failed to close SMTP connection", errors.Attr(err)) } }() diff --git a/pkg/sqlmigration/046_update_dashboard_alert_and_saved_view_v5.go b/pkg/sqlmigration/046_update_dashboard_alert_and_saved_view_v5.go index 2de6fa70d83..44ecd146262 100644 --- a/pkg/sqlmigration/046_update_dashboard_alert_and_saved_view_v5.go +++ b/pkg/sqlmigration/046_update_dashboard_alert_and_saved_view_v5.go @@ -6,12 +6,14 @@ import ( "encoding/json" "log/slog" + "github.com/uptrace/bun" + "github.com/uptrace/bun/migrate" + + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/telemetrystore" "github.com/SigNoz/signoz/pkg/transition" - "github.com/uptrace/bun" - "github.com/uptrace/bun/migrate" ) type queryBuilderV5Migration struct { @@ -59,7 +61,7 @@ func (migration *queryBuilderV5Migration) getTraceDuplicateKeys(ctx context.Cont rows, err := migration.telemetryStore.ClickhouseDB().Query(ctx, query) if err != nil { - migration.logger.WarnContext(ctx, "failed to query trace duplicate keys", "error", err) + migration.logger.WarnContext(ctx, "failed to query trace duplicate keys", errors.Attr(err)) return nil, nil } defer rows.Close() @@ -68,7 +70,7 @@ func (migration *queryBuilderV5Migration) getTraceDuplicateKeys(ctx context.Cont for rows.Next() { var key string if err := rows.Scan(&key); err != nil { - migration.logger.WarnContext(ctx, "failed to scan trace duplicate key", "error", err) + migration.logger.WarnContext(ctx, "failed to scan trace duplicate key", errors.Attr(err)) continue } keys = append(keys, key) @@ -90,7 +92,7 @@ func (migration *queryBuilderV5Migration) getLogDuplicateKeys(ctx context.Contex rows, err := migration.telemetryStore.ClickhouseDB().Query(ctx, query) if err != nil { - migration.logger.WarnContext(ctx, "failed to query log duplicate keys", "error", err) + migration.logger.WarnContext(ctx, "failed to query log duplicate keys", errors.Attr(err)) return nil, nil } defer rows.Close() @@ -99,7 +101,7 @@ func (migration *queryBuilderV5Migration) getLogDuplicateKeys(ctx context.Contex for rows.Next() { var key string if err := rows.Scan(&key); err != nil { - migration.logger.WarnContext(ctx, "failed to scan log duplicate key", "error", err) + migration.logger.WarnContext(ctx, "failed to scan log duplicate key", errors.Attr(err)) continue } keys = append(keys, key) @@ -277,7 +279,7 @@ func (migration *queryBuilderV5Migration) migrateRules( alertsMigrator := transition.NewAlertMigrateV5(migration.logger, logsKeys, tracesKeys) for _, rule := range rules { - migration.logger.InfoContext(ctx, "migrating rule", "rule_id", rule.ID) + migration.logger.InfoContext(ctx, "migrating rule", slog.String("rule_id", rule.ID)) updated := alertsMigrator.Migrate(ctx, rule.Data) diff --git a/pkg/sqlmigration/066_migrate_rules_v4_to_v5_post_deprecation.go b/pkg/sqlmigration/066_migrate_rules_v4_to_v5_post_deprecation.go index c1d91674732..f9e9a01d81a 100644 --- a/pkg/sqlmigration/066_migrate_rules_v4_to_v5_post_deprecation.go +++ b/pkg/sqlmigration/066_migrate_rules_v4_to_v5_post_deprecation.go @@ -6,12 +6,14 @@ import ( "encoding/json" "log/slog" + "github.com/uptrace/bun" + "github.com/uptrace/bun/migrate" + + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/telemetrystore" "github.com/SigNoz/signoz/pkg/transition" - "github.com/uptrace/bun" - "github.com/uptrace/bun/migrate" ) type migrateRulesV4ToV5 struct { @@ -55,7 +57,7 @@ func (migration *migrateRulesV4ToV5) getLogDuplicateKeys(ctx context.Context) ([ rows, err := migration.telemetryStore.ClickhouseDB().Query(ctx, query) if err != nil { - migration.logger.WarnContext(ctx, "failed to query log duplicate keys", "error", err) + migration.logger.WarnContext(ctx, "failed to query log duplicate keys", errors.Attr(err)) return nil, nil } defer rows.Close() @@ -64,7 +66,7 @@ func (migration *migrateRulesV4ToV5) getLogDuplicateKeys(ctx context.Context) ([ for rows.Next() { var key string if err := rows.Scan(&key); err != nil { - migration.logger.WarnContext(ctx, "failed to scan log duplicate key", "error", err) + migration.logger.WarnContext(ctx, "failed to scan log duplicate key", errors.Attr(err)) continue } keys = append(keys, key) @@ -85,7 +87,7 @@ func (migration *migrateRulesV4ToV5) getTraceDuplicateKeys(ctx context.Context) rows, err := migration.telemetryStore.ClickhouseDB().Query(ctx, query) if err != nil { - migration.logger.WarnContext(ctx, "failed to query trace duplicate keys", "error", err) + migration.logger.WarnContext(ctx, "failed to query trace duplicate keys", errors.Attr(err)) return nil, nil } defer rows.Close() @@ -94,7 +96,7 @@ func (migration *migrateRulesV4ToV5) getTraceDuplicateKeys(ctx context.Context) for rows.Next() { var key string if err := rows.Scan(&key); err != nil { - migration.logger.WarnContext(ctx, "failed to scan trace duplicate key", "error", err) + migration.logger.WarnContext(ctx, "failed to scan trace duplicate key", errors.Attr(err)) continue } keys = append(keys, key) @@ -151,10 +153,10 @@ func (migration *migrateRulesV4ToV5) Up(ctx context.Context, db *bun.DB) error { } if version == "" { - migration.logger.WarnContext(ctx, "unexpected empty version for rule", "rule_id", rule.ID) + migration.logger.WarnContext(ctx, "unexpected empty version for rule", slog.String("rule_id", rule.ID)) } - migration.logger.InfoContext(ctx, "migrating rule v4 to v5", "rule_id", rule.ID, "current_version", version) + migration.logger.InfoContext(ctx, "migrating rule v4 to v5", slog.String("rule_id", rule.ID), slog.String("current_version", version)) // Check if the queries envelope already exists and is non-empty hasQueriesEnvelope := false @@ -169,14 +171,14 @@ func (migration *migrateRulesV4ToV5) Up(ctx context.Context, db *bun.DB) error { if hasQueriesEnvelope { // already has queries envelope, just bump version // this is because user made a mistake of choosing version - migration.logger.InfoContext(ctx, "rule already has queries envelope, bumping version", "rule_id", rule.ID) + migration.logger.InfoContext(ctx, "rule already has queries envelope, bumping version", slog.String("rule_id", rule.ID)) rule.Data["version"] = "v5" } else { // old format, run full migration - migration.logger.InfoContext(ctx, "rule has old format, running full migration", "rule_id", rule.ID) + migration.logger.InfoContext(ctx, "rule has old format, running full migration", slog.String("rule_id", rule.ID)) updated := alertsMigrator.Migrate(ctx, rule.Data) if !updated { - migration.logger.WarnContext(ctx, "expected updated to be true but got false", "rule_id", rule.ID) + migration.logger.WarnContext(ctx, "expected updated to be true but got false", slog.String("rule_id", rule.ID)) continue } rule.Data["version"] = "v5" @@ -198,7 +200,7 @@ func (migration *migrateRulesV4ToV5) Up(ctx context.Context, db *bun.DB) error { count++ } if count != 0 { - migration.logger.InfoContext(ctx, "migrate v4 alerts", "count", count) + migration.logger.InfoContext(ctx, "migrate v4 alerts", slog.Int("count", count)) } return tx.Commit() diff --git a/pkg/sqlmigrator/migrator.go b/pkg/sqlmigrator/migrator.go index 8cf6c79013f..4a73d586258 100644 --- a/pkg/sqlmigrator/migrator.go +++ b/pkg/sqlmigrator/migrator.go @@ -2,13 +2,15 @@ package sqlmigrator import ( "context" + "log/slog" "time" "github.com/SigNoz/signoz/pkg/errors" + "github.com/uptrace/bun/migrate" + "github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/sqlstore" - "github.com/uptrace/bun/migrate" ) var ( @@ -41,7 +43,7 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, sqlstor } func (migrator *migrator) Migrate(ctx context.Context) error { - migrator.settings.Logger().InfoContext(ctx, "starting sqlstore migrations", "dialect", migrator.dialect) + migrator.settings.Logger().InfoContext(ctx, "starting sqlstore migrations", slog.String("dialect", migrator.dialect)) if err := migrator.migrator.Init(ctx); err != nil { return err } @@ -58,11 +60,11 @@ func (migrator *migrator) Migrate(ctx context.Context) error { } if group.IsZero() { - migrator.settings.Logger().InfoContext(ctx, "no new migrations to run (database is up to date)", "dialect", migrator.dialect) + migrator.settings.Logger().InfoContext(ctx, "no new migrations to run (database is up to date)", slog.String("dialect", migrator.dialect)) return nil } - migrator.settings.Logger().InfoContext(ctx, "migrated to", "group", group.String(), "dialect", migrator.dialect) + migrator.settings.Logger().InfoContext(ctx, "migrated to", slog.String("group", group.String()), slog.String("dialect", migrator.dialect)) return nil } @@ -78,17 +80,17 @@ func (migrator *migrator) Rollback(ctx context.Context) error { } if group.IsZero() { - migrator.settings.Logger().InfoContext(ctx, "no groups to roll back", "dialect", migrator.dialect) + migrator.settings.Logger().InfoContext(ctx, "no groups to roll back", slog.String("dialect", migrator.dialect)) return nil } - migrator.settings.Logger().InfoContext(ctx, "rolled back", "group", group.String(), "dialect", migrator.dialect) + migrator.settings.Logger().InfoContext(ctx, "rolled back", slog.String("group", group.String()), slog.String("dialect", migrator.dialect)) return nil } func (migrator *migrator) Lock(ctx context.Context) error { if err := migrator.migrator.Lock(ctx); err == nil { - migrator.settings.Logger().InfoContext(ctx, "acquired migration lock", "dialect", migrator.dialect) + migrator.settings.Logger().InfoContext(ctx, "acquired migration lock", slog.String("dialect", migrator.dialect)) return nil } @@ -102,15 +104,15 @@ func (migrator *migrator) Lock(ctx context.Context) error { select { case <-timer.C: err := errors.New(errors.TypeTimeout, errors.CodeTimeout, "timed out waiting for lock") - migrator.settings.Logger().ErrorContext(ctx, "cannot acquire lock", "error", err, "lock_timeout", migrator.config.Lock.Timeout.String(), "dialect", migrator.dialect) + migrator.settings.Logger().ErrorContext(ctx, "cannot acquire lock", errors.Attr(err), slog.String("lock_timeout", migrator.config.Lock.Timeout.String()), slog.String("dialect", migrator.dialect)) return err case <-ticker.C: var err error if err = migrator.migrator.Lock(ctx); err == nil { - migrator.settings.Logger().InfoContext(ctx, "acquired migration lock", "dialect", migrator.dialect) + migrator.settings.Logger().InfoContext(ctx, "acquired migration lock", slog.String("dialect", migrator.dialect)) return nil } - migrator.settings.Logger().ErrorContext(ctx, "attempt to acquire lock failed", "error", err, "lock_interval", migrator.config.Lock.Interval.String(), "dialect", migrator.dialect) + migrator.settings.Logger().ErrorContext(ctx, "attempt to acquire lock failed", errors.Attr(err), slog.String("lock_interval", migrator.config.Lock.Interval.String()), slog.String("dialect", migrator.dialect)) case <-ctx.Done(): return ctx.Err() } diff --git a/pkg/sqlschema/sqlitesqlschema/provider.go b/pkg/sqlschema/sqlitesqlschema/provider.go index 1b2b01f2652..c70b442f9d5 100644 --- a/pkg/sqlschema/sqlitesqlschema/provider.go +++ b/pkg/sqlschema/sqlitesqlschema/provider.go @@ -5,11 +5,12 @@ import ( "strconv" "strings" + "github.com/uptrace/bun" + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/sqlschema" "github.com/SigNoz/signoz/pkg/sqlstore" - "github.com/uptrace/bun" ) type provider struct { @@ -79,7 +80,7 @@ func (provider *provider) GetIndices(ctx context.Context, tableName sqlschema.Ta defer func() { if err := rows.Close(); err != nil { - provider.settings.Logger().ErrorContext(ctx, "error closing rows", "error", err) + provider.settings.Logger().ErrorContext(ctx, "error closing rows", errors.Attr(err)) } }() @@ -240,4 +241,3 @@ func isSQLiteIdentifierChar(ch byte) bool { (ch >= '0' && ch <= '9') || ch == '_' } - diff --git a/pkg/sqlstore/bun.go b/pkg/sqlstore/bun.go index b2df4d8a350..7e17b274e37 100644 --- a/pkg/sqlstore/bun.go +++ b/pkg/sqlstore/bun.go @@ -4,10 +4,11 @@ import ( "context" "database/sql" - "github.com/SigNoz/signoz/pkg/errors" - "github.com/SigNoz/signoz/pkg/factory" "github.com/uptrace/bun" "github.com/uptrace/bun/schema" + + "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/factory" ) type transactorKey struct{} @@ -42,7 +43,7 @@ func (db *BunDB) RunInTxCtx(ctx context.Context, opts *sql.TxOptions, cb func(ct defer func() { if err := tx.Rollback(); err != nil { if err != sql.ErrTxDone { - db.settings.Logger().ErrorContext(ctx, "cannot rollback transaction", "error", err) + db.settings.Logger().ErrorContext(ctx, "cannot rollback transaction", errors.Attr(err)) } } }() diff --git a/pkg/sqlstore/sqlitesqlstore/provider.go b/pkg/sqlstore/sqlitesqlstore/provider.go index 066f27abd29..3a068831741 100644 --- a/pkg/sqlstore/sqlitesqlstore/provider.go +++ b/pkg/sqlstore/sqlitesqlstore/provider.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "log/slog" "net/url" "github.com/SigNoz/signoz/pkg/errors" @@ -52,7 +53,7 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config if err != nil { return nil, err } - settings.Logger().InfoContext(ctx, "connected to sqlite", "path", config.Sqlite.Path) + settings.Logger().InfoContext(ctx, "connected to sqlite", slog.String("path", config.Sqlite.Path)) sqldb.SetMaxOpenConns(config.Connection.MaxOpenConns) sqliteDialect := sqlitedialect.New() diff --git a/pkg/sqlstore/sqlstorehook/logging.go b/pkg/sqlstore/sqlstorehook/logging.go index fc4b0154a67..68212e05ff7 100644 --- a/pkg/sqlstore/sqlstorehook/logging.go +++ b/pkg/sqlstore/sqlstorehook/logging.go @@ -31,12 +31,12 @@ func (*logging) BeforeQuery(ctx context.Context, event *bun.QueryEvent) context. } func (hook *logging) AfterQuery(ctx context.Context, event *bun.QueryEvent) { - hook.logger.Log( + hook.logger.LogAttrs( ctx, hook.level, "::SQLSTORE-QUERY::", - "db_query_operation", event.Operation(), - "db_query_text", event.Query, - "db_query_duration", time.Since(event.StartTime).String(), + slog.String("db_query_operation", event.Operation()), + slog.String("db_query_text", event.Query), + slog.String("db_query_duration", time.Since(event.StartTime).String()), ) } diff --git a/pkg/statsreporter/analyticsstatsreporter/provider.go b/pkg/statsreporter/analyticsstatsreporter/provider.go index 69395b1302f..647765a4c42 100644 --- a/pkg/statsreporter/analyticsstatsreporter/provider.go +++ b/pkg/statsreporter/analyticsstatsreporter/provider.go @@ -2,11 +2,16 @@ package analyticsstatsreporter import ( "context" + "log/slog" "sync" "time" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + "github.com/SigNoz/signoz/pkg/analytics" "github.com/SigNoz/signoz/pkg/analytics/segmentanalytics" + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/modules/organization" "github.com/SigNoz/signoz/pkg/modules/user" @@ -18,8 +23,6 @@ import ( "github.com/SigNoz/signoz/pkg/types/instrumentationtypes" "github.com/SigNoz/signoz/pkg/valuer" "github.com/SigNoz/signoz/pkg/version" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" ) type provider struct { @@ -100,7 +103,7 @@ func New( func (provider *provider) Start(ctx context.Context) error { go func() { if err := provider.analytics.Start(ctx); err != nil { - provider.settings.Logger().ErrorContext(ctx, "failed to start analytics", "error", err) + provider.settings.Logger().ErrorContext(ctx, "failed to start analytics", errors.Attr(err)) } }() @@ -116,7 +119,7 @@ func (provider *provider) Start(ctx context.Context) error { if err := provider.Report(ctx); err != nil { span.RecordError(err) - provider.settings.Logger().WarnContext(ctx, "failed to report stats", "error", err) + provider.settings.Logger().WarnContext(ctx, "failed to report stats", errors.Attr(err)) } span.End() @@ -133,7 +136,7 @@ func (provider *provider) Report(ctx context.Context) error { for _, org := range orgs { stats := provider.collectOrg(ctx, org.ID) if len(stats) == 0 { - provider.settings.Logger().WarnContext(ctx, "no stats collected", "org_id", org.ID) + provider.settings.Logger().WarnContext(ctx, "no stats collected", slog.Any("org_id", org.ID)) continue } @@ -153,7 +156,7 @@ func (provider *provider) Report(ctx context.Context) error { stats["created_at"] = org.CreatedAt stats["alias"] = org.Alias - provider.settings.Logger().DebugContext(ctx, "reporting stats", "stats", stats) + provider.settings.Logger().DebugContext(ctx, "reporting stats", slog.Any("stats", stats)) provider.analytics.IdentifyGroup(ctx, org.ID.String(), stats) provider.analytics.TrackGroup(ctx, org.ID.String(), "Stats Reported", stats) @@ -164,13 +167,13 @@ func (provider *provider) Report(ctx context.Context) error { users, err := provider.userGetter.ListByOrgID(ctx, org.ID) if err != nil { - provider.settings.Logger().WarnContext(ctx, "failed to list users", "error", err, "org_id", org.ID) + provider.settings.Logger().WarnContext(ctx, "failed to list users", errors.Attr(err), slog.Any("org_id", org.ID)) continue } maxLastObservedAtPerUserID, err := provider.tokenizer.ListMaxLastObservedAtByOrgID(ctx, org.ID) if err != nil { - provider.settings.Logger().WarnContext(ctx, "failed to list max last observed at per user id", "error", err, "org_id", org.ID) + provider.settings.Logger().WarnContext(ctx, "failed to list max last observed at per user id", errors.Attr(err), slog.Any("org_id", org.ID)) maxLastObservedAtPerUserID = make(map[valuer.UUID]time.Time) } @@ -192,11 +195,11 @@ func (provider *provider) Stop(ctx context.Context) error { close(provider.stopC) // report stats on stop if err := provider.Report(ctx); err != nil { - provider.settings.Logger().WarnContext(ctx, "failed to report stats", "error", err) + provider.settings.Logger().WarnContext(ctx, "failed to report stats", errors.Attr(err)) } if err := provider.analytics.Stop(ctx); err != nil { - provider.settings.Logger().ErrorContext(ctx, "failed to stop analytics", "error", err) + provider.settings.Logger().ErrorContext(ctx, "failed to stop analytics", errors.Attr(err)) } return nil @@ -219,7 +222,7 @@ func (provider *provider) collectOrg(ctx context.Context, orgID valuer.UUID) map collectorStats, err := collector.Collect(ctx, orgID) if err != nil { - provider.settings.Logger().ErrorContext(ctx, "failed to collect stats", "error", err) + provider.settings.Logger().ErrorContext(ctx, "failed to collect stats", errors.Attr(err)) return } diff --git a/pkg/telemetrylogs/statement_builder.go b/pkg/telemetrylogs/statement_builder.go index 1bce288aa28..59d2328630d 100644 --- a/pkg/telemetrylogs/statement_builder.go +++ b/pkg/telemetrylogs/statement_builder.go @@ -217,7 +217,7 @@ func (b *logQueryStatementBuilder) adjustKeys(ctx context.Context, keys map[stri for _, action := range actions { // TODO: change to debug level once we are confident about the behavior - b.logger.InfoContext(ctx, "key adjustment action", "action", action) + b.logger.InfoContext(ctx, "key adjustment action", slog.String("action", action)) } return query diff --git a/pkg/telemetrymetadata/body_json_metadata.go b/pkg/telemetrymetadata/body_json_metadata.go index 11d6938b4e1..ae7797dc4f6 100644 --- a/pkg/telemetrymetadata/body_json_metadata.go +++ b/pkg/telemetrymetadata/body_json_metadata.go @@ -3,6 +3,7 @@ package telemetrymetadata import ( "context" "fmt" + "log/slog" "reflect" "strings" "time" @@ -78,7 +79,7 @@ func (t *telemetryMetaStore) fetchBodyJSONPaths(ctx context.Context, for _, typ := range typesArray { mapping, found := telemetrytypes.MappingStringToJSONDataType[typ] if !found { - t.logger.ErrorContext(ctx, "failed to map type string to JSON data type", "type", typ, "path", path) + t.logger.ErrorContext(ctx, "failed to map type string to JSON data type", slog.String("type", typ), slog.String("path", path)) continue } fieldKeys = append(fieldKeys, &telemetrytypes.TelemetryFieldKey{ @@ -229,7 +230,7 @@ func (t *telemetryMetaStore) getJSONPathIndexes(ctx context.Context, paths ...st jsonDataType, found := telemetrytypes.MappingStringToJSONDataType[columnType] if !found { - t.logger.ErrorContext(ctx, "failed to map column type to JSON data type", "column_type", columnType, "column_expr", columnExpr) + t.logger.ErrorContext(ctx, "failed to map column type to JSON data type", slog.String("column_type", columnType), slog.String("column_expr", columnExpr)) continue } diff --git a/pkg/telemetrymetadata/metadata.go b/pkg/telemetrymetadata/metadata.go index 87f7c39d11b..a7673d16f7d 100644 --- a/pkg/telemetrymetadata/metadata.go +++ b/pkg/telemetrymetadata/metadata.go @@ -6,6 +6,9 @@ import ( "log/slog" "strings" + "github.com/huandu/go-sqlbuilder" + "golang.org/x/exp/maps" + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/querybuilder" @@ -18,8 +21,6 @@ import ( "github.com/SigNoz/signoz/pkg/types/metrictypes" qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" "github.com/SigNoz/signoz/pkg/types/telemetrytypes" - "github.com/huandu/go-sqlbuilder" - "golang.org/x/exp/maps" ) var ( @@ -582,7 +583,7 @@ func (t *telemetryMetaStore) getLogsKeys(ctx context.Context, fieldKeySelectors if querybuilder.BodyJSONQueryEnabled { bodyJSONPaths, finished, err := t.buildBodyJSONPaths(ctx, fieldKeySelectors) // LIKE for pattern matching if err != nil { - t.logger.ErrorContext(ctx, "failed to extract body JSON paths", "error", err) + t.logger.ErrorContext(ctx, "failed to extract body JSON paths", errors.Attr(err)) } keys = append(keys, bodyJSONPaths...) complete = complete && finished @@ -1066,7 +1067,7 @@ func (t *telemetryMetaStore) getRelatedValues(ctx context.Context, fieldValueSel if err == nil { sb.AddWhereClause(whereClause.WhereClause) } else { - t.logger.WarnContext(ctx, "error parsing existing query for related values", "error", err) + t.logger.WarnContext(ctx, "error parsing existing query for related values", errors.Attr(err)) } } @@ -1121,7 +1122,7 @@ func (t *telemetryMetaStore) getRelatedValues(ctx context.Context, fieldValueSel query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse) - t.logger.DebugContext(ctx, "query for related values", "query", query, "args", args) + t.logger.DebugContext(ctx, "query for related values", slog.String("query", query), slog.Any("args", args)) rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...) if err != nil { @@ -1760,7 +1761,7 @@ func (t *telemetryMetaStore) fetchMetricsTemporalityAndType(ctx context.Context, query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse) - t.logger.DebugContext(ctx, "fetching metric temporality", "query", query, "args", args) + t.logger.DebugContext(ctx, "fetching metric temporality", slog.String("query", query), slog.Any("args", args)) rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...) if err != nil { @@ -1816,7 +1817,7 @@ func (t *telemetryMetaStore) fetchMeterSourceMetricsTemporalityAndType(ctx conte query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse) - t.logger.DebugContext(ctx, "fetching meter metrics temporality", "query", query, "args", args) + t.logger.DebugContext(ctx, "fetching meter metrics temporality", slog.String("query", query), slog.Any("args", args)) rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...) if err != nil { @@ -1940,7 +1941,7 @@ func (t *telemetryMetaStore) FetchLastSeenInfoMulti(ctx context.Context, metricN query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse) - t.logger.DebugContext(ctx, "fetching metric last seen timestamp", "query", query, "args", args) + t.logger.DebugContext(ctx, "fetching metric last seen timestamp", slog.String("query", query), slog.Any("args", args)) rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...) if err != nil { diff --git a/pkg/telemetrytraces/statement_builder.go b/pkg/telemetrytraces/statement_builder.go index c833f20548f..1ef0f28bdc6 100644 --- a/pkg/telemetrytraces/statement_builder.go +++ b/pkg/telemetrytraces/statement_builder.go @@ -119,11 +119,11 @@ func (b *traceQueryStatementBuilder) Build( traceStart, traceEnd, ok := finder.GetTraceTimeRangeMulti(ctx, traceIDs) if !ok { - b.logger.DebugContext(ctx, "failed to get trace time range", "trace_ids", traceIDs) + b.logger.DebugContext(ctx, "failed to get trace time range", slog.Any("trace_ids", traceIDs)) } else if traceStart > 0 && traceEnd > 0 { start = uint64(traceStart) end = uint64(traceEnd) - b.logger.DebugContext(ctx, "optimized time range for traces", "trace_ids", traceIDs, "start", start, "end", end) + b.logger.DebugContext(ctx, "optimized time range for traces", slog.Any("trace_ids", traceIDs), slog.Uint64("start", start), slog.Uint64("end", end)) } } } @@ -249,7 +249,7 @@ func (b *traceQueryStatementBuilder) adjustKeys(ctx context.Context, keys map[st for _, action := range actions { // TODO: change to debug level once we are confident about the behavior - b.logger.InfoContext(ctx, "key adjustment action", "action", action) + b.logger.InfoContext(ctx, "key adjustment action", slog.String("action", action)) } return query diff --git a/pkg/telemetrytraces/trace_operator_cte_builder.go b/pkg/telemetrytraces/trace_operator_cte_builder.go index 472c98b2c0c..be4a7144dd2 100644 --- a/pkg/telemetrytraces/trace_operator_cte_builder.go +++ b/pkg/telemetrytraces/trace_operator_cte_builder.go @@ -3,13 +3,15 @@ package telemetrytraces import ( "context" "fmt" + "log/slog" "strings" + "github.com/huandu/go-sqlbuilder" + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/querybuilder" qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" "github.com/SigNoz/signoz/pkg/types/telemetrytypes" - "github.com/huandu/go-sqlbuilder" ) type cteNode struct { @@ -96,9 +98,9 @@ func (b *traceOperatorCTEBuilder) build(ctx context.Context, requestType qbtypes finalArgs := querybuilder.PrependArgs(cteArgs, finalStmt.Args) b.stmtBuilder.logger.DebugContext(ctx, "Final trace operator query built", - "operator_expression", b.operator.Expression, - "cte_count", len(cteFragments), - "args_count", len(finalArgs)) + slog.String("operator_expression", b.operator.Expression), + slog.Int("cte_count", len(cteFragments)), + slog.Int("args_count", len(finalArgs))) return &qbtypes.Statement{ Query: finalSQL, @@ -188,12 +190,12 @@ func (b *traceOperatorCTEBuilder) buildQueryCTE(ctx context.Context, queryName s } keySelectors := getKeySelectors(*query) - b.stmtBuilder.logger.DebugContext(ctx, "Key selectors for query", "query_name", queryName, "key_selectors", keySelectors) + b.stmtBuilder.logger.DebugContext(ctx, "Key selectors for query", slog.String("query_name", queryName), slog.Any("key_selectors", keySelectors)) keys, _, err := b.stmtBuilder.metadataStore.GetKeysMulti(ctx, keySelectors) if err != nil { return "", err } - b.stmtBuilder.logger.DebugContext(ctx, "Retrieved keys for query", "query_name", queryName, "keys_count", len(keys)) + b.stmtBuilder.logger.DebugContext(ctx, "Retrieved keys for query", slog.String("query_name", queryName), slog.Int("keys_count", len(keys))) // Build resource filter CTE for this specific query resourceFilterCTEName := fmt.Sprintf("__resource_filter_%s", cteName) @@ -204,11 +206,11 @@ func (b *traceOperatorCTEBuilder) buildQueryCTE(ctx context.Context, queryName s if resourceStmt != nil && resourceStmt.Query != "" { b.stmtBuilder.logger.DebugContext(ctx, "Built resource filter CTE for query", - "query_name", queryName, - "resource_filter_cte_name", resourceFilterCTEName) + slog.String("query_name", queryName), + slog.String("resource_filter_cte_name", resourceFilterCTEName)) b.addCTE(resourceFilterCTEName, resourceStmt.Query, resourceStmt.Args, nil) } else { - b.stmtBuilder.logger.DebugContext(ctx, "No resource filter needed for query", "query_name", queryName) + b.stmtBuilder.logger.DebugContext(ctx, "No resource filter needed for query", slog.String("query_name", queryName)) resourceFilterCTEName = "" } @@ -228,7 +230,7 @@ func (b *traceOperatorCTEBuilder) buildQueryCTE(ctx context.Context, queryName s ) if query.Filter != nil && query.Filter.Expression != "" { - b.stmtBuilder.logger.DebugContext(ctx, "Applying filter to query CTE", "query_name", queryName, "filter", query.Filter.Expression) + b.stmtBuilder.logger.DebugContext(ctx, "Applying filter to query CTE", slog.String("query_name", queryName), slog.String("filter", query.Filter.Expression)) filterWhereClause, err := querybuilder.PrepareWhereClause( query.Filter.Expression, querybuilder.FilterExprVisitorOpts{ @@ -240,27 +242,27 @@ func (b *traceOperatorCTEBuilder) buildQueryCTE(ctx context.Context, queryName s }, b.start, b.end, ) if err != nil { - b.stmtBuilder.logger.ErrorContext(ctx, "Failed to prepare where clause", "error", err, "filter", query.Filter.Expression) + b.stmtBuilder.logger.ErrorContext(ctx, "Failed to prepare where clause", errors.Attr(err), slog.String("filter", query.Filter.Expression)) return "", err } if filterWhereClause != nil { - b.stmtBuilder.logger.DebugContext(ctx, "Adding where clause", "where_clause", filterWhereClause.WhereClause) + b.stmtBuilder.logger.DebugContext(ctx, "Adding where clause", slog.Any("where_clause", filterWhereClause.WhereClause)) sb.AddWhereClause(filterWhereClause.WhereClause) } else { - b.stmtBuilder.logger.WarnContext(ctx, "PrepareWhereClause returned nil", "filter", query.Filter.Expression) + b.stmtBuilder.logger.WarnContext(ctx, "PrepareWhereClause returned nil", slog.String("filter", query.Filter.Expression)) } } else { if query.Filter == nil { - b.stmtBuilder.logger.DebugContext(ctx, "No filter for query CTE", "query_name", queryName, "reason", "filter is nil") + b.stmtBuilder.logger.DebugContext(ctx, "No filter for query CTE", slog.String("query_name", queryName), slog.String("reason", "filter is nil")) } else { - b.stmtBuilder.logger.DebugContext(ctx, "No filter for query CTE", "query_name", queryName, "reason", "filter expression is empty") + b.stmtBuilder.logger.DebugContext(ctx, "No filter for query CTE", slog.String("query_name", queryName), slog.String("reason", "filter expression is empty")) } } sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse) b.stmtBuilder.logger.DebugContext(ctx, "Built query CTE", - "query_name", queryName, - "cte_name", cteName) + slog.String("query_name", queryName), + slog.String("cte_name", cteName)) dependencies := []string{} if resourceFilterCTEName != "" { dependencies = append(dependencies, resourceFilterCTEName) @@ -317,10 +319,10 @@ func (b *traceOperatorCTEBuilder) buildOperatorCTE(ctx context.Context, op qbtyp } b.stmtBuilder.logger.DebugContext(ctx, "Built operator CTE", - "operator", op.StringValue(), - "cte_name", cteName, - "left_cte", leftCTE, - "right_cte", rightCTE) + slog.String("operator", op.StringValue()), + slog.String("cte_name", cteName), + slog.String("left_cte", leftCTE), + slog.String("right_cte", rightCTE)) b.addCTE(cteName, sql, args, dependsOn) return cteName, nil } @@ -453,7 +455,7 @@ func (b *traceOperatorCTEBuilder) buildListQuery(ctx context.Context, selectFrom colExpr, err := b.stmtBuilder.fm.ColumnExpressionFor(ctx, &field, keys) if err != nil { b.stmtBuilder.logger.WarnContext(ctx, "failed to map select field", - "field", field.Name, "error", err) + slog.String("field", field.Name), errors.Attr(err)) continue } sb.SelectMore(colExpr) @@ -759,7 +761,7 @@ func (b *traceOperatorCTEBuilder) buildTraceQuery(ctx context.Context, selectFro } else { b.stmtBuilder.logger.WarnContext(ctx, "ignoring order by field that's not available in trace context", - "field", orderBy.Key.Name) + slog.String("field", orderBy.Key.Name)) } } } diff --git a/pkg/telemetrytraces/trace_operator_statement_builder.go b/pkg/telemetrytraces/trace_operator_statement_builder.go index 6ee389d995d..4bebe235252 100644 --- a/pkg/telemetrytraces/trace_operator_statement_builder.go +++ b/pkg/telemetrytraces/trace_operator_statement_builder.go @@ -69,8 +69,8 @@ func (b *traceOperatorStatementBuilder) Build( } b.logger.DebugContext(ctx, "Building trace operator query", - "expression", query.Expression, - "request_type", requestType) + slog.String("expression", query.Expression), + slog.Any("request_type", requestType)) // Build the CTE-based query builder := &traceOperatorCTEBuilder{ diff --git a/pkg/tokenizer/jwttokenizer/provider.go b/pkg/tokenizer/jwttokenizer/provider.go index 51f5fc8c14c..524b4d423ef 100644 --- a/pkg/tokenizer/jwttokenizer/provider.go +++ b/pkg/tokenizer/jwttokenizer/provider.go @@ -2,18 +2,20 @@ package jwttokenizer import ( "context" + "log/slog" "slices" "sync" "time" + "github.com/dgraph-io/ristretto/v2" + "github.com/golang-jwt/jwt/v5" + "github.com/SigNoz/signoz/pkg/cache" "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/tokenizer" "github.com/SigNoz/signoz/pkg/types/authtypes" "github.com/SigNoz/signoz/pkg/valuer" - "github.com/dgraph-io/ristretto/v2" - "github.com/golang-jwt/jwt/v5" ) var ( @@ -44,7 +46,7 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/tokenizer/jwttokenizer") if config.JWT.Secret == "" { - settings.Logger().ErrorContext(ctx, "🚨 CRITICAL SECURITY ISSUE: No JWT secret key specified!", "error", "SIGNOZ_TOKENIZER_JWT_SECRET environment variable is not set. This has dire consequences for the security of the application. Without a JWT secret, user sessions are vulnerable to tampering and unauthorized access. Please set the SIGNOZ_TOKENIZER_JWT_SECRET environment variable immediately. For more information, please refer to https://github.com/SigNoz/signoz/issues/8400.") + settings.Logger().ErrorContext(ctx, "🚨 CRITICAL SECURITY ISSUE: No JWT secret key specified!", slog.String("error", "SIGNOZ_TOKENIZER_JWT_SECRET environment variable is not set. This has dire consequences for the security of the application. Without a JWT secret, user sessions are vulnerable to tampering and unauthorized access. Please set the SIGNOZ_TOKENIZER_JWT_SECRET environment variable immediately. For more information, please refer to https://github.com/SigNoz/signoz/issues/8400.")) } lastObservedAtCache, err := ristretto.NewCache(&ristretto.Config[string, map[valuer.UUID]time.Time]{ @@ -129,7 +131,7 @@ func (provider *provider) GetIdentity(ctx context.Context, accessToken string) ( } func (provider *provider) DeleteToken(ctx context.Context, accessToken string) error { - provider.settings.Logger().WarnContext(ctx, "Deleting token by access token is not supported for this tokenizer, this is a no-op", "tokenizer_provider", provider.config.Provider) + provider.settings.Logger().WarnContext(ctx, "Deleting token by access token is not supported for this tokenizer, this is a no-op", slog.String("tokenizer_provider", provider.config.Provider)) return nil } @@ -148,7 +150,7 @@ func (provider *provider) RotateToken(ctx context.Context, _ string, refreshToke } func (provider *provider) DeleteTokensByUserID(ctx context.Context, userID valuer.UUID) error { - provider.settings.Logger().WarnContext(ctx, "Deleting token by user id is not supported for this tokenizer, this is a no-op", "tokenizer_provider", provider.config.Provider) + provider.settings.Logger().WarnContext(ctx, "Deleting token by user id is not supported for this tokenizer, this is a no-op", slog.String("tokenizer_provider", provider.config.Provider)) return nil } @@ -160,7 +162,7 @@ func (provider *provider) DeleteIdentity(ctx context.Context, userID valuer.UUID func (provider *provider) SetLastObservedAt(ctx context.Context, accessToken string, lastObservedAt time.Time) error { claims, err := provider.getClaimsFromToken(accessToken) if err != nil { - provider.settings.Logger().ErrorContext(ctx, "failed to set last observed at", "error", err) + provider.settings.Logger().ErrorContext(ctx, "failed to set last observed at", errors.Attr(err)) return nil } @@ -176,7 +178,7 @@ func (provider *provider) SetLastObservedAt(ctx context.Context, accessToken str cachedLastObservedAts[valuer.MustNewUUID(claims.UserID)] = lastObservedAt if ok := provider.lastObservedAtCache.Set(claims.OrgID, cachedLastObservedAts, 1); !ok { - provider.settings.Logger().ErrorContext(ctx, "error caching last observed at timestamp", "user_id", claims.UserID) + provider.settings.Logger().ErrorContext(ctx, "error caching last observed at timestamp", slog.String("user_id", claims.UserID)) } return nil @@ -258,7 +260,7 @@ func (provider *provider) getOrSetIdentity(ctx context.Context, orgID, userID va err := provider.cache.Get(ctx, orgID, identityCacheKey(userID), identity) if err != nil && !errors.Ast(err, errors.TypeNotFound) { - provider.settings.Logger().ErrorContext(ctx, "failed to get identity from cache", "error", err) + provider.settings.Logger().ErrorContext(ctx, "failed to get identity from cache", errors.Attr(err)) } if err == nil { @@ -272,7 +274,7 @@ func (provider *provider) getOrSetIdentity(ctx context.Context, orgID, userID va err = provider.cache.Set(ctx, orgID, identityCacheKey(identity.UserID), identity, 0) if err != nil { - provider.settings.Logger().ErrorContext(ctx, "failed to cache identity", "error", err) + provider.settings.Logger().ErrorContext(ctx, "failed to cache identity", errors.Attr(err)) } return identity, nil diff --git a/pkg/tokenizer/opaquetokenizer/provider.go b/pkg/tokenizer/opaquetokenizer/provider.go index 73e04fc82e7..1823836fa26 100644 --- a/pkg/tokenizer/opaquetokenizer/provider.go +++ b/pkg/tokenizer/opaquetokenizer/provider.go @@ -2,9 +2,14 @@ package opaquetokenizer import ( "context" + "log/slog" "slices" "time" + "github.com/dgraph-io/ristretto/v2" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + "github.com/SigNoz/signoz/pkg/cache" "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/factory" @@ -14,9 +19,6 @@ import ( "github.com/SigNoz/signoz/pkg/types/authtypes" "github.com/SigNoz/signoz/pkg/types/cachetypes" "github.com/SigNoz/signoz/pkg/valuer" - "github.com/dgraph-io/ristretto/v2" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" ) var ( @@ -81,7 +83,7 @@ func (provider *provider) Start(ctx context.Context) error { orgs, err := provider.orgGetter.ListByOwnedKeyRange(ctx) if err != nil { - provider.settings.Logger().ErrorContext(ctx, "failed to get orgs data", "error", err) + provider.settings.Logger().ErrorContext(ctx, "failed to get orgs data", errors.Attr(err)) span.End() continue } @@ -89,12 +91,12 @@ func (provider *provider) Start(ctx context.Context) error { for _, org := range orgs { if err := provider.gc(ctx, org); err != nil { span.RecordError(err) - provider.settings.Logger().ErrorContext(ctx, "failed to garbage collect tokens", "error", err, "org_id", org.ID) + provider.settings.Logger().ErrorContext(ctx, "failed to garbage collect tokens", errors.Attr(err), slog.Any("org_id", org.ID)) } if err := provider.flushLastObservedAt(ctx, org); err != nil { span.RecordError(err) - provider.settings.Logger().ErrorContext(ctx, "failed to flush tokens", "error", err, "org_id", org.ID) + provider.settings.Logger().ErrorContext(ctx, "failed to flush tokens", errors.Attr(err), slog.Any("org_id", org.ID)) } } @@ -230,12 +232,12 @@ func (provider *provider) Stop(ctx context.Context) error { for _, org := range orgs { // garbage collect tokens on stop if err := provider.gc(ctx, org); err != nil { - provider.settings.Logger().ErrorContext(ctx, "failed to garbage collect tokens", "error", err, "org_id", org.ID) + provider.settings.Logger().ErrorContext(ctx, "failed to garbage collect tokens", errors.Attr(err), slog.Any("org_id", org.ID)) } // flush tokens on stop if err := provider.flushLastObservedAt(ctx, org); err != nil { - provider.settings.Logger().ErrorContext(ctx, "failed to flush tokens", "error", err, "org_id", org.ID) + provider.settings.Logger().ErrorContext(ctx, "failed to flush tokens", errors.Attr(err), slog.Any("org_id", org.ID)) } } @@ -254,7 +256,7 @@ func (provider *provider) SetLastObservedAt(ctx context.Context, accessToken str } if ok := provider.lastObservedAtCache.Set(lastObservedAtCacheKey(accessToken, token.UserID), lastObservedAt, 24); !ok { - provider.settings.Logger().ErrorContext(ctx, "error caching last observed at timestamp", "user_id", token.UserID) + provider.settings.Logger().ErrorContext(ctx, "error caching last observed at timestamp", slog.Any("user_id", token.UserID)) } err = provider.cache.Set(ctx, emptyOrgID, accessTokenCacheKey(accessToken), token, provider.config.Lifetime.Max) diff --git a/pkg/transition/migrate_alert.go b/pkg/transition/migrate_alert.go index c500ef4d541..020776c8682 100644 --- a/pkg/transition/migrate_alert.go +++ b/pkg/transition/migrate_alert.go @@ -35,11 +35,11 @@ func (m *alertMigrateV5) Migrate(ctx context.Context, ruleData map[string]any) b } if version == "v5" { - m.logger.InfoContext(ctx, "alert is already migrated to v5, skipping", "alert_name", ruleData["alert"]) + m.logger.InfoContext(ctx, "alert is already migrated to v5, skipping", slog.Any("alert_name", ruleData["alert"])) return false } - m.logger.InfoContext(ctx, "migrating alert", "alert_name", ruleData["alert"]) + m.logger.InfoContext(ctx, "migrating alert", slog.Any("alert_name", ruleData["alert"])) ruleCondition, ok := ruleData["condition"].(map[string]any) if !ok { @@ -80,7 +80,7 @@ func (m *alertMigrateV5) Migrate(ctx context.Context, ruleData map[string]any) b // wrap it in the v5 envelope envelope := m.WrapInV5Envelope(name, queryMap, "builder_query") - m.logger.InfoContext(ctx, "envelope after wrap", "envelope", envelope) + m.logger.InfoContext(ctx, "envelope after wrap", slog.Any("envelope", envelope)) compositeQuery["queries"] = append(compositeQuery["queries"].([]any), envelope) } } diff --git a/pkg/transition/migrate_common.go b/pkg/transition/migrate_common.go index 518e48417f4..a163a5ba714 100644 --- a/pkg/transition/migrate_common.go +++ b/pkg/transition/migrate_common.go @@ -196,7 +196,7 @@ func (mc *migrateCommon) updateQueryData(ctx context.Context, queryData map[stri } if !present { - mc.logger.WarnContext(ctx, "found a order by without group by, skipping", "order_col_name", columnName) + mc.logger.WarnContext(ctx, "found a order by without group by, skipping", slog.String("order_col_name", columnName)) continue } } @@ -512,7 +512,7 @@ func (mc *migrateCommon) createFilterExpression(ctx context.Context, queryData m expression := mc.buildExpression(ctx, items, op, dataSource) if expression != "" { if groupByExists := mc.groupByExistsExpr(queryData); groupByExists != "" && dataSource != "metrics" { - mc.logger.InfoContext(ctx, "adding default exists for old qb", "group_by_exists", groupByExists) + mc.logger.InfoContext(ctx, "adding default exists for old qb", slog.String("group_by_exists", groupByExists)) expression += " " + groupByExists } @@ -615,10 +615,10 @@ func (mc *migrateCommon) createHavingExpression(ctx context.Context, queryData m } } - mc.logger.InfoContext(ctx, "having before expression", "having", having) + mc.logger.InfoContext(ctx, "having before expression", slog.Any("having", having)) expression := mc.buildExpression(ctx, having, "AND", dataSource) - mc.logger.InfoContext(ctx, "having expression after building", "expression", expression, "having", having) + mc.logger.InfoContext(ctx, "having expression after building", slog.String("expression", expression), slog.Any("having", having)) queryData["having"] = map[string]any{ "expression": expression, } @@ -653,7 +653,7 @@ func (mc *migrateCommon) buildExpression(ctx context.Context, items []any, op, d } if slices.Contains(mc.ambiguity[dataSource], keyStr) { - mc.logger.WarnContext(ctx, "ambiguity found for a key", "ambiguity_key", keyStr) + mc.logger.WarnContext(ctx, "ambiguity found for a key", slog.String("ambiguity_key", keyStr)) typeStr, ok := key["type"].(string) if ok { if typeStr == "tag" { @@ -703,13 +703,13 @@ func (mc *migrateCommon) buildCondition(ctx context.Context, key, operator strin return fmt.Sprintf("%s <= %s", key, formattedValue) case "in", "IN": if !strings.HasPrefix(formattedValue, "[") && !mc.isVariable(formattedValue) { - mc.logger.WarnContext(ctx, "multi-value operator in found with single value", "key", key, "formatted_value", formattedValue) + mc.logger.WarnContext(ctx, "multi-value operator in found with single value", slog.String("key", key), slog.String("formatted_value", formattedValue)) return fmt.Sprintf("%s = %s", key, formattedValue) } return fmt.Sprintf("%s IN %s", key, formattedValue) case "nin", "NOT IN": if !strings.HasPrefix(formattedValue, "[") && !mc.isVariable(formattedValue) { - mc.logger.WarnContext(ctx, "multi-value operator not in found with single value", "key", key, "formatted_value", formattedValue) + mc.logger.WarnContext(ctx, "multi-value operator not in found with single value", slog.String("key", key), slog.String("formatted_value", formattedValue)) return fmt.Sprintf("%s != %s", key, formattedValue) } return fmt.Sprintf("%s NOT IN %s", key, formattedValue) @@ -852,12 +852,12 @@ func (mc *migrateCommon) formatValue(ctx context.Context, value any, dataType st switch v := value.(type) { case string: if mc.isVariable(v) { - mc.logger.InfoContext(ctx, "found a variable", "dashboard_variable", v) + mc.logger.InfoContext(ctx, "found a variable", slog.String("dashboard_variable", v)) return mc.normalizeVariable(ctx, v) } else { // if we didn't recognize something as variable but looks like has variable like value, double check if strings.Contains(v, "{") || strings.Contains(v, "[") || strings.Contains(v, "$") { - mc.logger.WarnContext(ctx, "variable like string found", "dashboard_variable", v) + mc.logger.WarnContext(ctx, "variable like string found", slog.String("dashboard_variable", v)) } } @@ -955,7 +955,7 @@ func (mc *migrateCommon) normalizeVariable(ctx context.Context, s string) string } if strings.Contains(varName, " ") { - mc.logger.InfoContext(ctx, "found white space in var name, replacing it", "dashboard_var_name", varName) + mc.logger.InfoContext(ctx, "found white space in var name, replacing it", slog.String("dashboard_var_name", varName)) varName = strings.ReplaceAll(varName, " ", "") } diff --git a/pkg/transition/migrate_dashboard.go b/pkg/transition/migrate_dashboard.go index 5272d1f4469..fa69992416e 100644 --- a/pkg/transition/migrate_dashboard.go +++ b/pkg/transition/migrate_dashboard.go @@ -33,11 +33,11 @@ func (m *dashboardMigrateV5) Migrate(ctx context.Context, dashboardData map[stri } if version == "v5" { - m.logger.InfoContext(ctx, "dashboard is already migrated to v5, skipping", "dashboard_name", dashboardData["title"]) + m.logger.InfoContext(ctx, "dashboard is already migrated to v5, skipping", slog.Any("dashboard_name", dashboardData["title"])) return false } - m.logger.InfoContext(ctx, "migrating dashboard", "dashboard_name", dashboardData["title"]) + m.logger.InfoContext(ctx, "migrating dashboard", slog.Any("dashboard_name", dashboardData["title"])) // if there is a white space in variable, replace it if variables, ok := dashboardData["variables"].(map[string]any); ok { @@ -46,7 +46,7 @@ func (m *dashboardMigrateV5) Migrate(ctx context.Context, dashboardData map[stri name, ok := varMap["name"].(string) if ok { if strings.Contains(name, " ") { - m.logger.InfoContext(ctx, "found a variable with space in map, replacing it", "name", name) + m.logger.InfoContext(ctx, "found a variable with space in map, replacing it", slog.String("name", name)) name = strings.ReplaceAll(name, " ", "") updated = true varMap["name"] = name @@ -78,7 +78,7 @@ func (migration *dashboardMigrateV5) updateWidget(ctx context.Context, widget ma if qType, ok := query["queryType"]; ok { if qType == "promql" || qType == "clickhouse_sql" { - migration.logger.InfoContext(ctx, "nothing to migrate for query type", "query_type", qType) + migration.logger.InfoContext(ctx, "nothing to migrate for query type", slog.Any("query_type", qType)) return false } } diff --git a/pkg/transition/migrate_saved_view.go b/pkg/transition/migrate_saved_view.go index c0e7d3a5e91..74448235f31 100644 --- a/pkg/transition/migrate_saved_view.go +++ b/pkg/transition/migrate_saved_view.go @@ -47,7 +47,7 @@ func (m *savedViewMigrateV5) Migrate(ctx context.Context, data map[string]any) b // wrap it in the v5 envelope envelope := m.WrapInV5Envelope(name, queryMap, "builder_query") - m.logger.InfoContext(ctx, "envelope after wrap", "envelope", envelope) + m.logger.InfoContext(ctx, "envelope after wrap", slog.Any("envelope", envelope)) data["queries"] = append(data["queries"].([]any), envelope) } } From a321ef8de8a5abb1d75d5a686e6e8405ad44d507 Mon Sep 17 00:00:00 2001 From: Pandey Date: Sun, 22 Mar 2026 22:46:23 +0530 Subject: [PATCH 05/78] refactor(instrumentation): flatten code source into code.filepath, code.function, code.lineno (#10667) Replace the nested `slog.Source` group with flat OTel semantic convention keys by adding a Source log handler wrapper that extracts source info from record.PC. Remove AddSource from the JSON handler since the wrapper handles it directly. --- pkg/instrumentation/logger.go | 9 +---- pkg/instrumentation/loghandler/source.go | 28 ++++++++++++++ pkg/instrumentation/loghandler/source_test.go | 37 +++++++++++++++++++ 3 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 pkg/instrumentation/loghandler/source.go create mode 100644 pkg/instrumentation/loghandler/source_test.go diff --git a/pkg/instrumentation/logger.go b/pkg/instrumentation/logger.go index 5663000a362..0f20b83ae1c 100644 --- a/pkg/instrumentation/logger.go +++ b/pkg/instrumentation/logger.go @@ -13,13 +13,7 @@ type zapToSlogConverter struct{} func NewLogger(config Config) *slog.Logger { logger := slog.New( loghandler.New( - slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: config.Logs.Level, AddSource: true, ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { - // This is more in line with OpenTelemetry semantic conventions - if a.Key == slog.SourceKey { - a.Key = "code" - return a - } - + slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: config.Logs.Level, ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { if a.Key == slog.TimeKey { a.Key = "timestamp" return a @@ -27,6 +21,7 @@ func NewLogger(config Config) *slog.Logger { return a }}), + loghandler.NewSource(), loghandler.NewCorrelation(), loghandler.NewFiltering(), loghandler.NewException(), diff --git a/pkg/instrumentation/loghandler/source.go b/pkg/instrumentation/loghandler/source.go new file mode 100644 index 00000000000..7d809d9cc4e --- /dev/null +++ b/pkg/instrumentation/loghandler/source.go @@ -0,0 +1,28 @@ +package loghandler + +import ( + "context" + "log/slog" + "runtime" +) + +type source struct{} + +func NewSource() *source { + return &source{} +} + +func (h *source) Wrap(next LogHandler) LogHandler { + return LogHandlerFunc(func(ctx context.Context, record slog.Record) error { + if record.PC != 0 { + frame, _ := runtime.CallersFrames([]uintptr{record.PC}).Next() + record.AddAttrs( + slog.String("code.filepath", frame.File), + slog.String("code.function", frame.Function), + slog.Int("code.lineno", frame.Line), + ) + } + + return next.Handle(ctx, record) + }) +} diff --git a/pkg/instrumentation/loghandler/source_test.go b/pkg/instrumentation/loghandler/source_test.go new file mode 100644 index 00000000000..e1c04189158 --- /dev/null +++ b/pkg/instrumentation/loghandler/source_test.go @@ -0,0 +1,37 @@ +package loghandler + +import ( + "bytes" + "context" + "encoding/json" + "log/slog" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSource(t *testing.T) { + src := NewSource() + + buf := bytes.NewBuffer(nil) + logger := slog.New(&handler{base: slog.NewJSONHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}), wrappers: []Wrapper{src}}) + + logger.InfoContext(context.Background(), "test") + + m := make(map[string]any) + err := json.Unmarshal(buf.Bytes(), &m) + require.NoError(t, err) + + assert.Contains(t, m, "code.filepath") + assert.Contains(t, m, "code.function") + assert.Contains(t, m, "code.lineno") + + assert.Contains(t, m["code.filepath"], "source_test.go") + assert.Contains(t, m["code.function"], "TestSource") + assert.NotZero(t, m["code.lineno"]) + + // Ensure the nested "source" key is not present. + assert.NotContains(t, m, "source") + assert.NotContains(t, m, "code") +} From 2b1da9aac296c14a21da911e52f758ae7cae8b7d Mon Sep 17 00:00:00 2001 From: Pandey Date: Mon, 23 Mar 2026 11:01:56 +0530 Subject: [PATCH 06/78] test: fix public dashboard query range integration test (#10672) --- tests/integration/src/dashboard/02_public_dashboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/src/dashboard/02_public_dashboard.py b/tests/integration/src/dashboard/02_public_dashboard.py index 0105211168d..7d1c1fd4a53 100644 --- a/tests/integration/src/dashboard/02_public_dashboard.py +++ b/tests/integration/src/dashboard/02_public_dashboard.py @@ -175,7 +175,7 @@ def test_public_dashboard_widget_query_range( ), json={ "timeRangeEnabled": False, - "defaultTimeRange": "10s", + "defaultTimeRange": "10m", }, headers={"Authorization": f"Bearer {admin_token}"}, timeout=2, From b811991f9d80bd95a1bbf5f9f81bfc40858b74b5 Mon Sep 17 00:00:00 2001 From: Pandey Date: Mon, 23 Mar 2026 11:55:26 +0530 Subject: [PATCH 07/78] feat(middleware): add panic recovery middleware (#10666) * feat(middleware): add panic recovery middleware with TypeFatal error type Add a global HTTP recovery middleware that catches panics, logs them with OTel exception semantic conventions via errors.Attr, and returns a safe user-facing error response. Introduce TypeFatal/CodeFatal for unrecoverable failures and WithStacktrace to attach pre-formatted stack traces to errors. Remove redundant per-handler panic recovery blocks in querier APIs. * style(errors): keep WithStacktrace call on same line in test * fix(middleware): replace fmt.Errorf with errors.New in recovery test * feat(middleware): add request context to panic recovery logs Capture request body before handler runs and include method, path, and body in panic recovery logs using OTel semconv attributes. Improve error message to direct users to GitHub issues or support. --- ee/querier/handler.go | 22 ---- ee/query-service/app/server.go | 2 +- pkg/errors/code.go | 1 + pkg/errors/errors.go | 21 +++- pkg/errors/errors_test.go | 12 ++ pkg/errors/stacktrace.go | 7 ++ pkg/errors/type.go | 1 + pkg/http/middleware/recovery.go | 63 ++++++++++ pkg/http/middleware/recovery_test.go | 164 +++++++++++++++++++++++++++ pkg/http/render/render.go | 2 + pkg/querier/api.go | 42 ------- pkg/query-service/app/server.go | 2 +- 12 files changed, 271 insertions(+), 68 deletions(-) create mode 100644 pkg/http/middleware/recovery.go create mode 100644 pkg/http/middleware/recovery_test.go diff --git a/ee/querier/handler.go b/ee/querier/handler.go index 68a37996b93..31ced99b99c 100644 --- a/ee/querier/handler.go +++ b/ee/querier/handler.go @@ -5,9 +5,7 @@ import ( "context" "encoding/json" "io" - "log/slog" "net/http" - "runtime/debug" anomalyV2 "github.com/SigNoz/signoz/ee/anomaly" "github.com/SigNoz/signoz/pkg/errors" @@ -55,26 +53,6 @@ func (h *handler) QueryRange(rw http.ResponseWriter, req *http.Request) { return } - defer func() { - if r := recover(); r != nil { - stackTrace := string(debug.Stack()) - - queryJSON, _ := json.Marshal(queryRangeRequest) - - h.set.Logger.ErrorContext(ctx, "panic in QueryRange", - slog.Any("error", r), - slog.Any("user", claims.UserID), - slog.String("payload", string(queryJSON)), - slog.String("stacktrace", stackTrace), - ) - - render.Error(rw, errors.NewInternalf( - errors.CodeInternal, - "Something went wrong on our end. It's not you, it's us. Our team is notified about it. Reach out to support if issue persists.", - )) - } - }() - if err := queryRangeRequest.Validate(); err != nil { render.Error(rw, err) return diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index 845cb7b4268..3b5f1ab37c7 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -211,6 +211,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h r := baseapp.NewRouter() am := middleware.NewAuthZ(s.signoz.Instrumentation.Logger(), s.signoz.Modules.OrgGetter, s.signoz.Authz) + r.Use(middleware.NewRecovery(s.signoz.Instrumentation.Logger()).Wrap) r.Use(otelmux.Middleware( "apiserver", otelmux.WithMeterProvider(s.signoz.Instrumentation.MeterProvider()), @@ -219,7 +220,6 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h otelmux.WithFilter(func(r *http.Request) bool { return !slices.Contains([]string{"/api/v1/health"}, r.URL.Path) }), - otelmux.WithPublicEndpoint(), )) r.Use(middleware.NewIdentN(s.signoz.IdentNResolver, s.signoz.Sharder, s.signoz.Instrumentation.Logger()).Wrap) r.Use(middleware.NewTimeout(s.signoz.Instrumentation.Logger(), diff --git a/pkg/errors/code.go b/pkg/errors/code.go index cbfa854f6ca..bd4cbb1b55a 100644 --- a/pkg/errors/code.go +++ b/pkg/errors/code.go @@ -16,6 +16,7 @@ var ( CodeCanceled = Code{"canceled"} CodeTimeout = Code{"timeout"} CodeUnknown = Code{"unknown"} + CodeFatal = Code{"fatal"} CodeLicenseUnavailable = Code{"license_unavailable"} ) diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go index 6a1a3089475..99933cac3b3 100644 --- a/pkg/errors/errors.go +++ b/pkg/errors/errors.go @@ -22,14 +22,31 @@ type base struct { // a denotes any additional error messages (if present). a []string // s contains the stacktrace captured at error creation time. - s stacktrace + s fmt.Stringer } // Stacktrace returns the stacktrace captured at error creation time, formatted as a string. func (b *base) Stacktrace() string { + if b.s == nil { + return "" + } return b.s.String() } +// WithStacktrace replaces the auto-captured stacktrace with a pre-formatted string +// and returns a new base error. +func (b *base) WithStacktrace(s string) *base { + return &base{ + t: b.t, + c: b.c, + m: b.m, + e: b.e, + u: b.u, + a: b.a, + s: rawStacktrace(s), + } +} + // base implements Error interface. func (b *base) Error() string { if b.e != nil { @@ -89,7 +106,7 @@ func Wrap(cause error, t typ, code Code, message string) *base { // WithAdditionalf adds an additional error message to the existing error. func WithAdditionalf(cause error, format string, args ...any) *base { t, c, m, e, u, a := Unwrapb(cause) - var s stacktrace + var s fmt.Stringer if original, ok := cause.(*base); ok { s = original.s } diff --git a/pkg/errors/errors_test.go b/pkg/errors/errors_test.go index d07329892c0..9560b9f15df 100644 --- a/pkg/errors/errors_test.go +++ b/pkg/errors/errors_test.go @@ -58,3 +58,15 @@ func TestAttr(t *testing.T) { assert.Equal(t, "exception", attr.Key) assert.Equal(t, err, attr.Value.Any()) } + +func TestWithStacktrace(t *testing.T) { + err := New(TypeInternal, MustNewCode("test_code"), "panic").WithStacktrace("custom stack trace") + + assert.Equal(t, "custom stack trace", err.Stacktrace()) + assert.Equal(t, "panic", err.Error()) + + typ, code, message, _, _, _ := Unwrapb(err) + assert.Equal(t, TypeInternal, typ) + assert.Equal(t, "test_code", code.String()) + assert.Equal(t, "panic", message) +} diff --git a/pkg/errors/stacktrace.go b/pkg/errors/stacktrace.go index 6a6ddc61243..9de53e587a6 100644 --- a/pkg/errors/stacktrace.go +++ b/pkg/errors/stacktrace.go @@ -36,3 +36,10 @@ func (s stacktrace) String() string { } return buf.String() } + +// rawStacktrace holds a pre-formatted stacktrace string. +type rawStacktrace string + +func (r rawStacktrace) String() string { + return string(r) +} diff --git a/pkg/errors/type.go b/pkg/errors/type.go index 5a4c452848d..246c1f53d65 100644 --- a/pkg/errors/type.go +++ b/pkg/errors/type.go @@ -12,6 +12,7 @@ var ( TypeCanceled = typ{"canceled"} TypeTimeout = typ{"timeout"} TypeUnexpected = typ{"unexpected"} // Generic mismatch of expectations + TypeFatal = typ{"fatal"} // Unrecoverable failure (e.g. panic) TypeLicenseUnavailable = typ{"license-unavailable"} ) diff --git a/pkg/http/middleware/recovery.go b/pkg/http/middleware/recovery.go new file mode 100644 index 00000000000..f08a82e34db --- /dev/null +++ b/pkg/http/middleware/recovery.go @@ -0,0 +1,63 @@ +package middleware + +import ( + "bytes" + "fmt" + "io" + "log/slog" + "net/http" + "runtime" + + "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/http/render" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" +) + +const ( + recoveryMessage string = "::PANIC-RECOVERED::" +) + +type Recovery struct { + logger *slog.Logger +} + +func NewRecovery(logger *slog.Logger) *Recovery { + return &Recovery{ + logger: logger.With(slog.String("pkg", pkgname)), + } +} + +func (middleware *Recovery) Wrap(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + // Capture the request body before the handler consumes it. + var requestBody []byte + if req.Body != nil { + if body, err := io.ReadAll(req.Body); err == nil { + requestBody = body + } + req.Body = io.NopCloser(bytes.NewReader(requestBody)) + } + + defer func() { + if r := recover(); r != nil { + buf := make([]byte, 4096) + n := runtime.Stack(buf, false) + + err := errors.New(errors.TypeFatal, errors.CodeFatal, fmt.Sprint(r)).WithStacktrace(string(buf[:n])) + + attrs := []any{ + errors.Attr(err), + string(semconv.HTTPRequestMethodKey), req.Method, + string(semconv.HTTPRouteKey), req.URL.Path, + } + if len(requestBody) > 0 { + attrs = append(attrs, "request.body", string(requestBody)) + } + middleware.logger.ErrorContext(req.Context(), recoveryMessage, attrs...) + + render.Error(rw, errors.Wrap(err, errors.TypeFatal, errors.CodeFatal, "An unexpected error occurred. Please retry, and if the issue persists, report it at https://github.com/SigNoz/signoz/issues or contact support.")) + } + }() + next.ServeHTTP(rw, req) + }) +} diff --git a/pkg/http/middleware/recovery_test.go b/pkg/http/middleware/recovery_test.go new file mode 100644 index 00000000000..e22fcb3774b --- /dev/null +++ b/pkg/http/middleware/recovery_test.go @@ -0,0 +1,164 @@ +package middleware + +import ( + "context" + "encoding/json" + "log/slog" + "net/http" + "net/http/httptest" + "testing" + + "github.com/SigNoz/signoz/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRecovery(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + handler http.HandlerFunc + wantStatus int + wantLog bool + wantMessage string + wantErrorStatus bool + }{ + { + name: "PanicWithString", + handler: func(w http.ResponseWriter, r *http.Request) { + panic("something went wrong") + }, + wantStatus: http.StatusInternalServerError, + wantLog: true, + wantMessage: "something went wrong", + wantErrorStatus: true, + }, + { + name: "PanicWithError", + handler: func(w http.ResponseWriter, r *http.Request) { + panic(errors.New(errors.TypeInternal, errors.CodeInternal, "db connection failed")) + }, + wantStatus: http.StatusInternalServerError, + wantLog: true, + wantMessage: "db connection failed", + wantErrorStatus: true, + }, + { + name: "PanicWithInteger", + handler: func(w http.ResponseWriter, r *http.Request) { + panic(42) + }, + wantStatus: http.StatusInternalServerError, + wantLog: true, + wantMessage: "42", + wantErrorStatus: true, + }, + { + name: "PanicWithDivisionByZero", + handler: func(w http.ResponseWriter, r *http.Request) { + divisor := 0 + _ = 1 / divisor + }, + wantStatus: http.StatusInternalServerError, + wantLog: true, + wantMessage: "runtime error: integer divide by zero", + wantErrorStatus: true, + }, + { + name: "NoPanic", + handler: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }, + wantStatus: http.StatusOK, + wantLog: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + var records []slog.Record + logger := slog.New(newRecordCollector(&records)) + + m := NewRecovery(logger) + handler := m.Wrap(http.HandlerFunc(tc.handler)) + + rr := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/", nil) + handler.ServeHTTP(rr, req) + + assert.Equal(t, tc.wantStatus, rr.Code) + + if !tc.wantLog { + assert.Empty(t, records) + return + } + + require.Len(t, records, 1) + + err := extractException(t, records[0]) + require.NotNil(t, err) + + typ, _, message, _, _, _ := errors.Unwrapb(err) + assert.Equal(t, errors.TypeFatal, typ) + assert.Equal(t, tc.wantMessage, message) + + type stacktracer interface { + Stacktrace() string + } + st, ok := err.(stacktracer) + require.True(t, ok, "error should implement stacktracer") + assert.NotEmpty(t, st.Stacktrace()) + + if tc.wantErrorStatus { + var body map[string]any + require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &body)) + assert.Equal(t, "error", body["status"]) + } + }) + } +} + +// extractException finds the "exception" attr in a log record and returns the error. +func extractException(t *testing.T, record slog.Record) error { + t.Helper() + var found error + record.Attrs(func(a slog.Attr) bool { + if a.Key == "exception" { + if err, ok := a.Value.Any().(error); ok { + found = err + return false + } + } + return true + }) + return found +} + +// recordCollector is an slog.Handler that collects records for assertion. +type recordCollector struct { + records *[]slog.Record + attrs []slog.Attr +} + +func newRecordCollector(records *[]slog.Record) *recordCollector { + return &recordCollector{records: records} +} + +func (h *recordCollector) Enabled(_ context.Context, _ slog.Level) bool { return true } + +func (h *recordCollector) Handle(_ context.Context, record slog.Record) error { + for _, a := range h.attrs { + record.AddAttrs(a) + } + *h.records = append(*h.records, record) + return nil +} + +func (h *recordCollector) WithAttrs(attrs []slog.Attr) slog.Handler { + return &recordCollector{records: h.records, attrs: append(h.attrs, attrs...)} +} + +func (h *recordCollector) WithGroup(_ string) slog.Handler { return h } diff --git a/pkg/http/render/render.go b/pkg/http/render/render.go index c0d0fb793b1..d7827e25671 100644 --- a/pkg/http/render/render.go +++ b/pkg/http/render/render.go @@ -64,6 +64,8 @@ func Error(rw http.ResponseWriter, cause error) { httpCode = statusClientClosedConnection case errors.TypeTimeout: httpCode = http.StatusGatewayTimeout + case errors.TypeFatal: + httpCode = http.StatusInternalServerError case errors.TypeLicenseUnavailable: httpCode = http.StatusUnavailableForLegalReasons } diff --git a/pkg/querier/api.go b/pkg/querier/api.go index e7cf902a908..4dbc4d4172b 100644 --- a/pkg/querier/api.go +++ b/pkg/querier/api.go @@ -5,9 +5,7 @@ import ( "context" "encoding/json" "fmt" - "log/slog" "net/http" - "runtime/debug" "strconv" "github.com/SigNoz/signoz/pkg/analytics" @@ -52,26 +50,6 @@ func (handler *handler) QueryRange(rw http.ResponseWriter, req *http.Request) { return } - defer func() { - if r := recover(); r != nil { - stackTrace := string(debug.Stack()) - - queryJSON, _ := json.Marshal(queryRangeRequest) - - handler.set.Logger.ErrorContext(ctx, "panic in QueryRange", - slog.Any("error", r), - slog.Any("user", claims.UserID), - slog.String("payload", string(queryJSON)), - slog.String("stacktrace", stackTrace), - ) - - render.Error(rw, errors.NewInternalf( - errors.CodeInternal, - "Something went wrong on our end. It's not you, it's us. Our team is notified about it. Reach out to support if issue persists.", - )) - } - }() - // Validate the query request if err := queryRangeRequest.Validate(); err != nil { render.Error(rw, err) @@ -152,26 +130,6 @@ func (handler *handler) QueryRawStream(rw http.ResponseWriter, req *http.Request return } - defer func() { - if r := recover(); r != nil { - stackTrace := string(debug.Stack()) - - queryJSON, _ := json.Marshal(queryRangeRequest) - - handler.set.Logger.ErrorContext(ctx, "panic in QueryRawStream", - slog.Any("error", r), - slog.Any("user", claims.UserID), - slog.String("payload", string(queryJSON)), - slog.String("stacktrace", stackTrace), - ) - - render.Error(rw, errors.NewInternalf( - errors.CodeInternal, - "Something went wrong on our end. It's not you, it's us. Our team is notified about it. Reach out to support if issue persists.", - )) - } - }() - // Validate the query request if err := queryRangeRequest.Validate(); err != nil { render.Error(rw, err) diff --git a/pkg/query-service/app/server.go b/pkg/query-service/app/server.go index bff516d6264..eeb9c0212ca 100644 --- a/pkg/query-service/app/server.go +++ b/pkg/query-service/app/server.go @@ -190,6 +190,7 @@ func (s Server) HealthCheckStatus() chan healthcheck.Status { func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server, error) { r := NewRouter() + r.Use(middleware.NewRecovery(s.signoz.Instrumentation.Logger()).Wrap) r.Use(otelmux.Middleware( "apiserver", otelmux.WithMeterProvider(s.signoz.Instrumentation.MeterProvider()), @@ -198,7 +199,6 @@ func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server, otelmux.WithFilter(func(r *http.Request) bool { return !slices.Contains([]string{"/api/v1/health"}, r.URL.Path) }), - otelmux.WithPublicEndpoint(), )) r.Use(middleware.NewIdentN(s.signoz.IdentNResolver, s.signoz.Sharder, s.signoz.Instrumentation.Logger()).Wrap) r.Use(middleware.NewTimeout(s.signoz.Instrumentation.Logger(), From 807211b8d81f29831c0cd71e40df3e3345b6e31e Mon Sep 17 00:00:00 2001 From: Pandey Date: Mon, 23 Mar 2026 14:43:05 +0530 Subject: [PATCH 08/78] refactor(pprof): extract infrastructure provider (#10673) * refactor(pprof): extract infrastructure provider * refactor(pprof): remove redundant exports * chore: address comments --- conf/example.yaml | 7 +++ ee/query-service/app/server.go | 10 ---- pkg/pprof/config.go | 32 +++++++++++++ pkg/pprof/config_test.go | 42 +++++++++++++++++ pkg/pprof/httppprof/provider.go | 59 ++++++++++++++++++++++++ pkg/pprof/nooppprof/provider.go | 35 ++++++++++++++ pkg/pprof/pprof.go | 8 ++++ pkg/query-service/app/server.go | 10 ---- pkg/query-service/constants/constants.go | 1 - pkg/signoz/config.go | 5 ++ pkg/signoz/provider.go | 10 ++++ pkg/signoz/signoz.go | 12 +++++ 12 files changed, 210 insertions(+), 21 deletions(-) create mode 100644 pkg/pprof/config.go create mode 100644 pkg/pprof/config_test.go create mode 100644 pkg/pprof/httppprof/provider.go create mode 100644 pkg/pprof/nooppprof/provider.go create mode 100644 pkg/pprof/pprof.go diff --git a/conf/example.yaml b/conf/example.yaml index 4bce8dc3e6a..6a9c0eab850 100644 --- a/conf/example.yaml +++ b/conf/example.yaml @@ -39,6 +39,13 @@ instrumentation: host: "0.0.0.0" port: 9090 +##################### PProf ##################### +pprof: + # Whether to enable the pprof server. + enabled: true + # The address on which the pprof server listens. + address: 0.0.0.0:6060 + ##################### Web ##################### web: # Whether to enable the web frontend diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index 3b5f1ab37c7..ddb8e167078 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -5,7 +5,6 @@ import ( "fmt" "net" "net/http" - _ "net/http/pprof" // http profiler "slices" "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux" @@ -313,15 +312,6 @@ func (s *Server) Start(ctx context.Context) error { s.unavailableChannel <- healthcheck.Unavailable }() - go func() { - slog.Info("Starting pprof server", "addr", baseconst.DebugHttpPort) - - err = http.ListenAndServe(baseconst.DebugHttpPort, nil) - if err != nil { - slog.Error("Could not start pprof server", errors.Attr(err)) - } - }() - go func() { slog.Info("Starting OpAmp Websocket server", "addr", baseconst.OpAmpWsEndpoint) err := s.opampServer.Start(baseconst.OpAmpWsEndpoint) diff --git a/pkg/pprof/config.go b/pkg/pprof/config.go new file mode 100644 index 00000000000..6dcc5088834 --- /dev/null +++ b/pkg/pprof/config.go @@ -0,0 +1,32 @@ +package pprof + +import "github.com/SigNoz/signoz/pkg/factory" + +// Config holds the configuration for the pprof server. +type Config struct { + Enabled bool `mapstructure:"enabled"` + Address string `mapstructure:"address"` +} + +func NewConfigFactory() factory.ConfigFactory { + return factory.NewConfigFactory(factory.MustNewName("pprof"), newConfig) +} + +func newConfig() factory.Config { + return Config{ + Enabled: true, + Address: "0.0.0.0:6060", + } +} + +func (c Config) Validate() error { + return nil +} + +func (c Config) Provider() string { + if c.Enabled { + return "http" + } + + return "noop" +} diff --git a/pkg/pprof/config_test.go b/pkg/pprof/config_test.go new file mode 100644 index 00000000000..3106556ede8 --- /dev/null +++ b/pkg/pprof/config_test.go @@ -0,0 +1,42 @@ +package pprof + +import ( + "context" + "testing" + + "github.com/SigNoz/signoz/pkg/config" + "github.com/SigNoz/signoz/pkg/config/envprovider" + "github.com/SigNoz/signoz/pkg/factory" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewWithEnvProvider(t *testing.T) { + t.Setenv("SIGNOZ_PPROF_ENABLED", "false") + t.Setenv("SIGNOZ_PPROF_ADDRESS", "127.0.0.1:6061") + + conf, err := config.New( + context.Background(), + config.ResolverConfig{ + Uris: []string{"env:"}, + ProviderFactories: []config.ProviderFactory{ + envprovider.NewFactory(), + }, + }, + []factory.ConfigFactory{ + NewConfigFactory(), + }, + ) + require.NoError(t, err) + + actual := Config{} + err = conf.Unmarshal("pprof", &actual) + require.NoError(t, err) + + expected := Config{ + Enabled: false, + Address: "127.0.0.1:6061", + } + + assert.Equal(t, expected, actual) +} diff --git a/pkg/pprof/httppprof/provider.go b/pkg/pprof/httppprof/provider.go new file mode 100644 index 00000000000..fa96a75bb67 --- /dev/null +++ b/pkg/pprof/httppprof/provider.go @@ -0,0 +1,59 @@ +package httppprof + +import ( + "context" + "log/slog" + "net/http" + nethttppprof "net/http/pprof" + runtimepprof "runtime/pprof" + + "github.com/SigNoz/signoz/pkg/factory" + httpserver "github.com/SigNoz/signoz/pkg/http/server" + "github.com/SigNoz/signoz/pkg/pprof" +) + +type provider struct { + server *httpserver.Server +} + +func NewFactory() factory.ProviderFactory[pprof.PProf, pprof.Config] { + return factory.NewProviderFactory(factory.MustNewName("http"), New) +} + +func New(_ context.Context, settings factory.ProviderSettings, config pprof.Config) (pprof.PProf, error) { + server, err := httpserver.New( + settings.Logger.With(slog.String("pkg", "github.com/SigNoz/signoz/pkg/pprof/httppprof")), + httpserver.Config{Address: config.Address}, + newHandler(), + ) + if err != nil { + return nil, err + } + + return &provider{server: server}, nil +} + +func (provider *provider) Start(ctx context.Context) error { + return provider.server.Start(ctx) +} + +func (provider *provider) Stop(ctx context.Context) error { + return provider.server.Stop(ctx) +} + +func newHandler() http.Handler { + mux := http.NewServeMux() + // Register the endpoints from net/http/pprof. + mux.HandleFunc("/debug/pprof/", nethttppprof.Index) + mux.HandleFunc("/debug/pprof/cmdline", nethttppprof.Cmdline) + mux.HandleFunc("/debug/pprof/profile", nethttppprof.Profile) + mux.HandleFunc("/debug/pprof/symbol", nethttppprof.Symbol) + mux.HandleFunc("/debug/pprof/trace", nethttppprof.Trace) + + // Register the runtime profiles in the same order returned by runtime/pprof.Profiles(). + for _, profile := range runtimepprof.Profiles() { + mux.Handle("/debug/pprof/"+profile.Name(), nethttppprof.Handler(profile.Name())) + } + + return mux +} diff --git a/pkg/pprof/nooppprof/provider.go b/pkg/pprof/nooppprof/provider.go new file mode 100644 index 00000000000..a89e7407df0 --- /dev/null +++ b/pkg/pprof/nooppprof/provider.go @@ -0,0 +1,35 @@ +package nooppprof + +import ( + "context" + "sync" + + "github.com/SigNoz/signoz/pkg/factory" + "github.com/SigNoz/signoz/pkg/pprof" +) + +type provider struct { + stopC chan struct{} + stopOnce sync.Once +} + +func NewFactory() factory.ProviderFactory[pprof.PProf, pprof.Config] { + return factory.NewProviderFactory(factory.MustNewName("noop"), New) +} + +func New(_ context.Context, _ factory.ProviderSettings, _ pprof.Config) (pprof.PProf, error) { + return &provider{stopC: make(chan struct{})}, nil +} + +func (provider *provider) Start(context.Context) error { + <-provider.stopC + return nil +} + +func (provider *provider) Stop(context.Context) error { + provider.stopOnce.Do(func() { + close(provider.stopC) + }) + + return nil +} diff --git a/pkg/pprof/pprof.go b/pkg/pprof/pprof.go new file mode 100644 index 00000000000..af1a8d78750 --- /dev/null +++ b/pkg/pprof/pprof.go @@ -0,0 +1,8 @@ +package pprof + +import "github.com/SigNoz/signoz/pkg/factory" + +// PProf is the interface that wraps the pprof service lifecycle. +type PProf interface { + factory.Service +} diff --git a/pkg/query-service/app/server.go b/pkg/query-service/app/server.go index eeb9c0212ca..854be97e925 100644 --- a/pkg/query-service/app/server.go +++ b/pkg/query-service/app/server.go @@ -5,7 +5,6 @@ import ( "fmt" "net" "net/http" - _ "net/http/pprof" // http profiler "slices" "github.com/SigNoz/signoz/pkg/cache/memorycache" @@ -294,15 +293,6 @@ func (s *Server) Start(ctx context.Context) error { s.unavailableChannel <- healthcheck.Unavailable }() - go func() { - slog.Info("Starting pprof server", "addr", constants.DebugHttpPort) - - err = http.ListenAndServe(constants.DebugHttpPort, nil) - if err != nil { - slog.Error("Could not start pprof server", errors.Attr(err)) - } - }() - go func() { slog.Info("Starting OpAmp Websocket server", "addr", constants.OpAmpWsEndpoint) err := s.opampServer.Start(constants.OpAmpWsEndpoint) diff --git a/pkg/query-service/constants/constants.go b/pkg/query-service/constants/constants.go index e19e70f1a11..bf2345f808d 100644 --- a/pkg/query-service/constants/constants.go +++ b/pkg/query-service/constants/constants.go @@ -14,7 +14,6 @@ import ( const ( HTTPHostPort = "0.0.0.0:8080" // Address to serve http (query service) PrivateHostPort = "0.0.0.0:8085" // Address to server internal services like alert manager - DebugHttpPort = "0.0.0.0:6060" // Address to serve http (pprof) OpAmpWsEndpoint = "0.0.0.0:4320" // address for opamp websocket ) diff --git a/pkg/signoz/config.go b/pkg/signoz/config.go index c175bb04990..ee23d0d919b 100644 --- a/pkg/signoz/config.go +++ b/pkg/signoz/config.go @@ -23,6 +23,7 @@ import ( "github.com/SigNoz/signoz/pkg/instrumentation" "github.com/SigNoz/signoz/pkg/modules/metricsexplorer" "github.com/SigNoz/signoz/pkg/modules/user" + "github.com/SigNoz/signoz/pkg/pprof" "github.com/SigNoz/signoz/pkg/prometheus" "github.com/SigNoz/signoz/pkg/querier" "github.com/SigNoz/signoz/pkg/ruler" @@ -50,6 +51,9 @@ type Config struct { // Instrumentation config Instrumentation instrumentation.Config `mapstructure:"instrumentation"` + // PProf config + PProf pprof.Config `mapstructure:"pprof"` + // Analytics config Analytics analytics.Config `mapstructure:"analytics"` @@ -122,6 +126,7 @@ func NewConfig(ctx context.Context, logger *slog.Logger, resolverConfig config.R global.NewConfigFactory(), version.NewConfigFactory(), instrumentation.NewConfigFactory(), + pprof.NewConfigFactory(), analytics.NewConfigFactory(), web.NewConfigFactory(), cache.NewConfigFactory(), diff --git a/pkg/signoz/provider.go b/pkg/signoz/provider.go index 0f9066fdf02..542acbc856d 100644 --- a/pkg/signoz/provider.go +++ b/pkg/signoz/provider.go @@ -34,6 +34,9 @@ import ( "github.com/SigNoz/signoz/pkg/modules/session/implsession" "github.com/SigNoz/signoz/pkg/modules/user" "github.com/SigNoz/signoz/pkg/modules/user/impluser" + "github.com/SigNoz/signoz/pkg/pprof" + "github.com/SigNoz/signoz/pkg/pprof/httppprof" + "github.com/SigNoz/signoz/pkg/pprof/nooppprof" "github.com/SigNoz/signoz/pkg/prometheus" "github.com/SigNoz/signoz/pkg/prometheus/clickhouseprometheus" "github.com/SigNoz/signoz/pkg/querier" @@ -89,6 +92,13 @@ func NewWebProviderFactories() factory.NamedMap[factory.ProviderFactory[web.Web, ) } +func NewPProfProviderFactories() factory.NamedMap[factory.ProviderFactory[pprof.PProf, pprof.Config]] { + return factory.MustNewNamedMap( + httppprof.NewFactory(), + nooppprof.NewFactory(), + ) +} + func NewSQLStoreProviderFactories() factory.NamedMap[factory.ProviderFactory[sqlstore.SQLStore, sqlstore.Config]] { return factory.MustNewNamedMap( sqlitesqlstore.NewFactory(sqlstorehook.NewLoggingFactory(), sqlstorehook.NewInstrumentationFactory()), diff --git a/pkg/signoz/signoz.go b/pkg/signoz/signoz.go index cf51b108e9e..0b18814d50d 100644 --- a/pkg/signoz/signoz.go +++ b/pkg/signoz/signoz.go @@ -107,6 +107,17 @@ func New( // Get the provider settings from instrumentation providerSettings := instrumentation.ToProviderSettings() + pprofService, err := factory.NewProviderFromNamedMap( + ctx, + providerSettings, + config.PProf, + NewPProfProviderFactories(), + config.PProf.Provider(), + ) + if err != nil { + return nil, err + } + // Initialize analytics just after instrumentation, as providers might require it analytics, err := factory.NewProviderFromNamedMap( ctx, @@ -448,6 +459,7 @@ func New( registry, err := factory.NewRegistry( instrumentation.Logger(), factory.NewNamedService(factory.MustNewName("instrumentation"), instrumentation), + factory.NewNamedService(factory.MustNewName("pprof"), pprofService), factory.NewNamedService(factory.MustNewName("analytics"), analytics), factory.NewNamedService(factory.MustNewName("alertmanager"), alertmanager), factory.NewNamedService(factory.MustNewName("licensing"), licensing), From efbeca23cff00adb66685ada9d8ed011b3ba49f3 Mon Sep 17 00:00:00 2001 From: Piyush Singariya Date: Mon, 23 Mar 2026 18:20:27 +0530 Subject: [PATCH 09/78] chore: prepend normalize pipeline (#10627) * fix: prepend normalize pipeline * fix: don't save normalize pipeline in config --- .../app/logparsingpipeline/controller.go | 75 ++++++++++--------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/pkg/query-service/app/logparsingpipeline/controller.go b/pkg/query-service/app/logparsingpipeline/controller.go index 1e11c507de1..21655f906d9 100644 --- a/pkg/query-service/app/logparsingpipeline/controller.go +++ b/pkg/query-service/app/logparsingpipeline/controller.go @@ -132,38 +132,32 @@ func (ic *LogParsingPipelineController) ValidatePipelines(ctx context.Context, return err } -func (ic *LogParsingPipelineController) getDefaultPipelines() ([]pipelinetypes.GettablePipeline, error) { - defaultPipelines := []pipelinetypes.GettablePipeline{} - if querybuilder.BodyJSONQueryEnabled { - preprocessingPipeline := pipelinetypes.GettablePipeline{ - StoreablePipeline: pipelinetypes.StoreablePipeline{ - Name: "Default Pipeline - PreProcessing Body", - Alias: "NormalizeBodyDefault", - Enabled: true, - }, - Filter: &v3.FilterSet{ - Items: []v3.FilterItem{ - { - Key: v3.AttributeKey{ - Key: "body", - }, - Operator: v3.FilterOperatorExists, +func (ic *LogParsingPipelineController) getNormalizePipeline() pipelinetypes.GettablePipeline { + return pipelinetypes.GettablePipeline{ + StoreablePipeline: pipelinetypes.StoreablePipeline{ + Name: "Default Pipeline - PreProcessing Body", + Alias: "NormalizeBodyDefault", + Enabled: true, + }, + Filter: &v3.FilterSet{ + Items: []v3.FilterItem{ + { + Key: v3.AttributeKey{ + Key: "body", }, + Operator: v3.FilterOperatorExists, }, }, - Config: []pipelinetypes.PipelineOperator{ - { - ID: uuid.NewString(), - Type: "normalize", - Enabled: true, - If: "body != nil", - }, + }, + Config: []pipelinetypes.PipelineOperator{ + { + ID: uuid.NewString(), + Type: "normalize", + Enabled: true, + If: "body != nil", }, - } - - defaultPipelines = append(defaultPipelines, preprocessingPipeline) + }, } - return defaultPipelines, nil } // Returns effective list of pipelines including user created @@ -279,6 +273,17 @@ func (pc *LogParsingPipelineController) AgentFeatureType() agentConf.AgentFeatur } // Implements agentConf.AgentFeature interface. +// RecommendAgentConfig generates the collector config to be sent to agents. +// The normalize pipeline (when BodyJSONQueryEnabled) is injected here, after +// rawPipelineData is serialized. So it is only present in the config sent to +// the collector and never persisted to the database as part of the user's pipeline list. +// +// NOTE: The configId sent to agents is derived from the pipeline version number +// (e.g. "LogPipelines:5"), not the YAML content. If server-side logic changes +// the generated YAML without bumping the version (e.g. toggling BodyJSONQueryEnabled +// or updating operator IfExpressions), agents that already applied that version will +// not re-apply the new config. In such cases, users must save a new pipeline version +// via the API to force agents to pick up the change. func (pc *LogParsingPipelineController) RecommendAgentConfig( orgId valuer.UUID, currentConfYaml []byte, @@ -296,21 +301,19 @@ func (pc *LogParsingPipelineController) RecommendAgentConfig( return nil, "", err } - // recommend default pipelines along with user created pipelines - defaultPipelines, err := pc.getDefaultPipelines() + rawPipelineData, err := json.Marshal(pipelinesResp.Pipelines) if err != nil { - return nil, "", model.InternalError(fmt.Errorf("failed to get default pipelines: %w", err)) + return nil, "", errors.WrapInternalf(err, CodeRawPipelinesMarshalFailed, "could not serialize pipelines to JSON") } - pipelinesResp.Pipelines = append(pipelinesResp.Pipelines, defaultPipelines...) - updatedConf, err := GenerateCollectorConfigWithPipelines(currentConfYaml, pipelinesResp.Pipelines) - if err != nil { - return nil, "", err + if querybuilder.BodyJSONQueryEnabled { + // add default normalize pipeline at the beginning, only for sending to collector + pipelinesResp.Pipelines = append([]pipelinetypes.GettablePipeline{pc.getNormalizePipeline()}, pipelinesResp.Pipelines...) } - rawPipelineData, err := json.Marshal(pipelinesResp.Pipelines) + updatedConf, err := GenerateCollectorConfigWithPipelines(currentConfYaml, pipelinesResp.Pipelines) if err != nil { - return nil, "", errors.WrapInternalf(err, CodeRawPipelinesMarshalFailed, "could not serialize pipelines to JSON") + return nil, "", err } return updatedConf, string(rawPipelineData), nil From b0eec8132ba58b7548e085ed73f21b02408b98cb Mon Sep 17 00:00:00 2001 From: Karan Balani <29383381+balanikaran@users.noreply.github.com> Date: Mon, 23 Mar 2026 19:06:20 +0530 Subject: [PATCH 10/78] feat: introduce user_role table (#10664) * feat: introduce user_role table * fix: golint and register migrations * fix: user types and order of update user * feat: add migration to drop role column from users table * fix: raw queries pointing to role column in users table * chore: remove storable user struct and minor other changes * chore: remove refs of calling vars as storable users * chore: user 0th role instead of highest * chore: address pr comments * chore: rename userrolestore to user_role_store * chore: return userroles with user in getter where possible * chore: move user module as user setter * chore: arrange getter and setter methods * fix: nil pointer for update user in integration test due to half payload being passed * chore: update openapi specs * fix: nil errors without making frontend changes * fix: empty array check everywhere for user roles array and minor other changes * fix: imports * fix: rebase changes * chore: renaming functions * chore: simplified getorcreateuser user setter method and call sites * fix: golint * fix: remove redundant authz migration, remove fk enforcement for drop migration * fix: add new event for user activation --- docs/api/openapi.yml | 37 +- ee/query-service/app/api/cloudIntegrations.go | 16 +- .../api/generated/services/sigNoz.schemas.ts | 53 ++- .../src/api/generated/services/users/index.ts | 33 +- pkg/apiserver/signozapiserver/user.go | 10 +- pkg/authn/authnstore/sqlauthnstore/store.go | 21 +- .../passwordauthn/emailpasswordauthn/authn.go | 10 +- pkg/identn/impersonationidentn/provider.go | 10 +- pkg/modules/session/implsession/module.go | 26 +- pkg/modules/user/impluser/getter.go | 121 +++++- pkg/modules/user/impluser/handler.go | 44 +-- pkg/modules/user/impluser/service.go | 74 +++- .../user/impluser/{module.go => setter.go} | 364 +++++++++++------- pkg/modules/user/impluser/store.go | 42 +- pkg/modules/user/impluser/user_role_store.go | 83 ++++ pkg/modules/user/option.go | 8 + pkg/modules/user/user.go | 29 +- pkg/query-service/app/http_handler.go | 2 +- pkg/signoz/handler_test.go | 6 +- pkg/signoz/module.go | 9 +- pkg/signoz/module_test.go | 6 +- pkg/signoz/provider.go | 4 +- pkg/signoz/provider_test.go | 12 +- pkg/signoz/signoz.go | 16 +- pkg/sqlmigration/071_add_user_role.go | 195 ++++++++++ pkg/sqlmigration/072_drop_user_role_column.go | 73 ++++ .../analyticsstatsreporter/provider.go | 2 +- .../tokenizerstore/sqltokenizerstore/store.go | 22 +- pkg/types/authtypes/authn.go | 2 +- pkg/types/authtypes/role.go | 6 + pkg/types/authtypes/user_role.go | 62 +++ pkg/types/user.go | 66 +++- .../src/passwordauthn/08_user_unique_index.py | 15 +- 33 files changed, 1127 insertions(+), 352 deletions(-) rename pkg/modules/user/impluser/{module.go => setter.go} (60%) create mode 100644 pkg/modules/user/impluser/user_role_store.go create mode 100644 pkg/sqlmigration/071_add_user_role.go create mode 100644 pkg/sqlmigration/072_drop_user_role_column.go create mode 100644 pkg/types/authtypes/user_role.go diff --git a/docs/api/openapi.yml b/docs/api/openapi.yml index ca8e219a226..6a5386d7fb8 100644 --- a/docs/api/openapi.yml +++ b/docs/api/openapi.yml @@ -2026,6 +2026,31 @@ components: userId: type: string type: object + TypesDeprecatedUser: + properties: + createdAt: + format: date-time + type: string + displayName: + type: string + email: + type: string + id: + type: string + isRoot: + type: boolean + orgId: + type: string + role: + type: string + status: + type: string + updatedAt: + format: date-time + type: string + required: + - id + type: object TypesGettableAPIKey: properties: createdAt: @@ -2222,8 +2247,6 @@ components: type: boolean orgId: type: string - role: - type: string status: type: string updatedAt: @@ -5077,7 +5100,7 @@ paths: properties: data: items: - $ref: '#/components/schemas/TypesUser' + $ref: '#/components/schemas/TypesDeprecatedUser' type: array status: type: string @@ -5175,7 +5198,7 @@ paths: schema: properties: data: - $ref: '#/components/schemas/TypesUser' + $ref: '#/components/schemas/TypesDeprecatedUser' status: type: string required: @@ -5229,7 +5252,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/TypesUser' + $ref: '#/components/schemas/TypesDeprecatedUser' responses: "200": content: @@ -5237,7 +5260,7 @@ paths: schema: properties: data: - $ref: '#/components/schemas/TypesUser' + $ref: '#/components/schemas/TypesDeprecatedUser' status: type: string required: @@ -5295,7 +5318,7 @@ paths: schema: properties: data: - $ref: '#/components/schemas/TypesUser' + $ref: '#/components/schemas/TypesDeprecatedUser' status: type: string required: diff --git a/ee/query-service/app/api/cloudIntegrations.go b/ee/query-service/app/api/cloudIntegrations.go index d773841d765..2f1473d12f1 100644 --- a/ee/query-service/app/api/cloudIntegrations.go +++ b/ee/query-service/app/api/cloudIntegrations.go @@ -10,6 +10,8 @@ import ( "strings" "time" + "log/slog" + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/http/render" "github.com/SigNoz/signoz/pkg/modules/user" @@ -18,7 +20,6 @@ import ( "github.com/SigNoz/signoz/pkg/types/authtypes" "github.com/SigNoz/signoz/pkg/valuer" "github.com/gorilla/mux" - "log/slog" ) type CloudIntegrationConnectionParamsResponse struct { @@ -126,7 +127,7 @@ func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId )) } - allPats, err := ah.Signoz.Modules.User.ListAPIKeys(ctx, orgIdUUID) + allPats, err := ah.Signoz.Modules.UserSetter.ListAPIKeys(ctx, orgIdUUID) if err != nil { return "", basemodel.InternalError(fmt.Errorf( "couldn't list PATs: %w", err, @@ -154,7 +155,7 @@ func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId )) } - err = ah.Signoz.Modules.User.CreateAPIKey(ctx, newPAT) + err = ah.Signoz.Modules.UserSetter.CreateAPIKey(ctx, newPAT) if err != nil { return "", basemodel.InternalError(fmt.Errorf( "couldn't create cloud integration PAT: %w", err, @@ -169,14 +170,19 @@ func (ah *APIHandler) getOrCreateCloudIntegrationUser( cloudIntegrationUserName := fmt.Sprintf("%s-integration", cloudProvider) email := valuer.MustNewEmail(fmt.Sprintf("%s@signoz.io", cloudIntegrationUserName)) - cloudIntegrationUser, err := types.NewUser(cloudIntegrationUserName, email, types.RoleViewer, valuer.MustNewUUID(orgId), types.UserStatusActive) + cloudIntegrationUser, err := types.NewUser(cloudIntegrationUserName, email, valuer.MustNewUUID(orgId), types.UserStatusActive) if err != nil { return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration user: %w", err)) } password := types.MustGenerateFactorPassword(cloudIntegrationUser.ID.StringValue()) - cloudIntegrationUser, err = ah.Signoz.Modules.User.GetOrCreateUser(ctx, cloudIntegrationUser, user.WithFactorPassword(password)) + cloudIntegrationUser, err = ah.Signoz.Modules.UserSetter.GetOrCreateUser( + ctx, + cloudIntegrationUser, + user.WithFactorPassword(password), + user.WithRoleNames([]string{authtypes.SigNozViewerRoleName}), + ) if err != nil { return nil, basemodel.InternalError(fmt.Errorf("couldn't look for integration user: %w", err)) } diff --git a/frontend/src/api/generated/services/sigNoz.schemas.ts b/frontend/src/api/generated/services/sigNoz.schemas.ts index d51900606b5..a47d988d21b 100644 --- a/frontend/src/api/generated/services/sigNoz.schemas.ts +++ b/frontend/src/api/generated/services/sigNoz.schemas.ts @@ -2384,6 +2384,47 @@ export interface TypesChangePasswordRequestDTO { userId?: string; } +export interface TypesDeprecatedUserDTO { + /** + * @type string + * @format date-time + */ + createdAt?: Date; + /** + * @type string + */ + displayName?: string; + /** + * @type string + */ + email?: string; + /** + * @type string + */ + id: string; + /** + * @type boolean + */ + isRoot?: boolean; + /** + * @type string + */ + orgId?: string; + /** + * @type string + */ + role?: string; + /** + * @type string + */ + status?: string; + /** + * @type string + * @format date-time + */ + updatedAt?: Date; +} + export interface TypesGettableAPIKeyDTO { /** * @type string @@ -2682,10 +2723,6 @@ export interface TypesUserDTO { * @type string */ orgId?: string; - /** - * @type string - */ - role?: string; /** * @type string */ @@ -3266,7 +3303,7 @@ export type ListUsers200 = { /** * @type array */ - data: TypesUserDTO[]; + data: TypesDeprecatedUserDTO[]; /** * @type string */ @@ -3280,7 +3317,7 @@ export type GetUserPathParameters = { id: string; }; export type GetUser200 = { - data: TypesUserDTO; + data: TypesDeprecatedUserDTO; /** * @type string */ @@ -3291,7 +3328,7 @@ export type UpdateUserPathParameters = { id: string; }; export type UpdateUser200 = { - data: TypesUserDTO; + data: TypesDeprecatedUserDTO; /** * @type string */ @@ -3299,7 +3336,7 @@ export type UpdateUser200 = { }; export type GetMyUser200 = { - data: TypesUserDTO; + data: TypesDeprecatedUserDTO; /** * @type string */ diff --git a/frontend/src/api/generated/services/users/index.ts b/frontend/src/api/generated/services/users/index.ts index 7fcccb7e62b..99c95895235 100644 --- a/frontend/src/api/generated/services/users/index.ts +++ b/frontend/src/api/generated/services/users/index.ts @@ -34,13 +34,13 @@ import type { RenderErrorResponseDTO, RevokeAPIKeyPathParameters, TypesChangePasswordRequestDTO, + TypesDeprecatedUserDTO, TypesPostableAPIKeyDTO, TypesPostableBulkInviteRequestDTO, TypesPostableForgotPasswordDTO, TypesPostableInviteDTO, TypesPostableResetPasswordDTO, TypesStorableAPIKeyDTO, - TypesUserDTO, UpdateAPIKeyPathParameters, UpdateUser200, UpdateUserPathParameters, @@ -1093,13 +1093,13 @@ export const invalidateGetUser = async ( */ export const updateUser = ( { id }: UpdateUserPathParameters, - typesUserDTO: BodyType, + typesDeprecatedUserDTO: BodyType, ) => { return GeneratedAPIInstance({ url: `/api/v1/user/${id}`, method: 'PUT', headers: { 'Content-Type': 'application/json' }, - data: typesUserDTO, + data: typesDeprecatedUserDTO, }); }; @@ -1110,13 +1110,19 @@ export const getUpdateUserMutationOptions = < mutation?: UseMutationOptions< Awaited>, TError, - { pathParams: UpdateUserPathParameters; data: BodyType }, + { + pathParams: UpdateUserPathParameters; + data: BodyType; + }, TContext >; }): UseMutationOptions< Awaited>, TError, - { pathParams: UpdateUserPathParameters; data: BodyType }, + { + pathParams: UpdateUserPathParameters; + data: BodyType; + }, TContext > => { const mutationKey = ['updateUser']; @@ -1130,7 +1136,10 @@ export const getUpdateUserMutationOptions = < const mutationFn: MutationFunction< Awaited>, - { pathParams: UpdateUserPathParameters; data: BodyType } + { + pathParams: UpdateUserPathParameters; + data: BodyType; + } > = (props) => { const { pathParams, data } = props ?? {}; @@ -1143,7 +1152,7 @@ export const getUpdateUserMutationOptions = < export type UpdateUserMutationResult = NonNullable< Awaited> >; -export type UpdateUserMutationBody = BodyType; +export type UpdateUserMutationBody = BodyType; export type UpdateUserMutationError = ErrorType; /** @@ -1156,13 +1165,19 @@ export const useUpdateUser = < mutation?: UseMutationOptions< Awaited>, TError, - { pathParams: UpdateUserPathParameters; data: BodyType }, + { + pathParams: UpdateUserPathParameters; + data: BodyType; + }, TContext >; }): UseMutationResult< Awaited>, TError, - { pathParams: UpdateUserPathParameters; data: BodyType }, + { + pathParams: UpdateUserPathParameters; + data: BodyType; + }, TContext > => { const mutationOptions = getUpdateUserMutationOptions(options); diff --git a/pkg/apiserver/signozapiserver/user.go b/pkg/apiserver/signozapiserver/user.go index 0e24c5a4684..64fd1513ad1 100644 --- a/pkg/apiserver/signozapiserver/user.go +++ b/pkg/apiserver/signozapiserver/user.go @@ -118,7 +118,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error { Description: "This endpoint lists all users", Request: nil, RequestContentType: "", - Response: make([]*types.GettableUser, 0), + Response: make([]*types.DeprecatedUser, 0), ResponseContentType: "application/json", SuccessStatusCode: http.StatusOK, ErrorStatusCodes: []int{}, @@ -135,7 +135,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error { Description: "This endpoint returns the user I belong to", Request: nil, RequestContentType: "", - Response: new(types.GettableUser), + Response: new(types.DeprecatedUser), ResponseContentType: "application/json", SuccessStatusCode: http.StatusOK, ErrorStatusCodes: []int{}, @@ -152,7 +152,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error { Description: "This endpoint returns the user by id", Request: nil, RequestContentType: "", - Response: new(types.GettableUser), + Response: new(types.DeprecatedUser), ResponseContentType: "application/json", SuccessStatusCode: http.StatusOK, ErrorStatusCodes: []int{http.StatusNotFound}, @@ -167,9 +167,9 @@ func (provider *provider) addUserRoutes(router *mux.Router) error { Tags: []string{"users"}, Summary: "Update user", Description: "This endpoint updates the user by id", - Request: new(types.User), + Request: new(types.DeprecatedUser), RequestContentType: "application/json", - Response: new(types.GettableUser), + Response: new(types.DeprecatedUser), ResponseContentType: "application/json", SuccessStatusCode: http.StatusOK, ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound}, diff --git a/pkg/authn/authnstore/sqlauthnstore/store.go b/pkg/authn/authnstore/sqlauthnstore/store.go index 975e61c1ef6..cc9ebb12baa 100644 --- a/pkg/authn/authnstore/sqlauthnstore/store.go +++ b/pkg/authn/authnstore/sqlauthnstore/store.go @@ -3,6 +3,7 @@ package sqlauthnstore import ( "context" + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types/authtypes" @@ -17,7 +18,7 @@ func NewStore(sqlstore sqlstore.SQLStore) authtypes.AuthNStore { return &store{sqlstore: sqlstore} } -func (store *store) GetActiveUserAndFactorPasswordByEmailAndOrgID(ctx context.Context, email string, orgID valuer.UUID) (*types.User, *types.FactorPassword, error) { +func (store *store) GetActiveUserAndFactorPasswordByEmailAndOrgID(ctx context.Context, email string, orgID valuer.UUID) (*types.User, *types.FactorPassword, []*authtypes.UserRole, error) { user := new(types.User) factorPassword := new(types.FactorPassword) @@ -31,7 +32,7 @@ func (store *store) GetActiveUserAndFactorPasswordByEmailAndOrgID(ctx context.Co Where("status = ?", types.UserStatusActive.StringValue()). Scan(ctx) if err != nil { - return nil, nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrCodeUserNotFound, "user with email %s in org %s not found", email, orgID) + return nil, nil, nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrCodeUserNotFound, "user with email %s in org %s not found", email, orgID) } err = store. @@ -42,10 +43,22 @@ func (store *store) GetActiveUserAndFactorPasswordByEmailAndOrgID(ctx context.Co Where("user_id = ?", user.ID). Scan(ctx) if err != nil { - return nil, nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrCodePasswordNotFound, "user with email %s in org %s does not have password", email, orgID) + return nil, nil, nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrCodePasswordNotFound, "user with email %s in org %s does not have password", email, orgID) } - return user, factorPassword, nil + userRoles := make([]*authtypes.UserRole, 0) + err = store.sqlstore. + BunDBCtx(ctx). + NewSelect(). + Model(&userRoles). + Where("user_id = ?", user.ID). + Relation("Role"). + Scan(ctx) + if err != nil { + return nil, nil, nil, errors.Newf(errors.TypeInternal, errors.CodeInternal, "failed to get user roles for user %s in org %s", email, orgID) + } + + return user, factorPassword, userRoles, nil } func (store *store) GetAuthDomainFromID(ctx context.Context, domainID valuer.UUID) (*authtypes.AuthDomain, error) { diff --git a/pkg/authn/passwordauthn/emailpasswordauthn/authn.go b/pkg/authn/passwordauthn/emailpasswordauthn/authn.go index 1f0df7876c3..b05fb49beef 100644 --- a/pkg/authn/passwordauthn/emailpasswordauthn/authn.go +++ b/pkg/authn/passwordauthn/emailpasswordauthn/authn.go @@ -21,7 +21,7 @@ func New(store authtypes.AuthNStore) *AuthN { } func (a *AuthN) Authenticate(ctx context.Context, email string, password string, orgID valuer.UUID) (*authtypes.Identity, error) { - user, factorPassword, err := a.store.GetActiveUserAndFactorPasswordByEmailAndOrgID(ctx, email, orgID) + user, factorPassword, userRoles, err := a.store.GetActiveUserAndFactorPasswordByEmailAndOrgID(ctx, email, orgID) if err != nil { return nil, err } @@ -30,5 +30,11 @@ func (a *AuthN) Authenticate(ctx context.Context, email string, password string, return nil, errors.New(errors.TypeUnauthenticated, types.ErrCodeIncorrectPassword, "invalid email or password") } - return authtypes.NewIdentity(user.ID, orgID, user.Email, user.Role, authtypes.IdentNProviderTokenizer), nil + if len(userRoles) == 0 { + return nil, errors.New(errors.TypeUnexpected, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found") + } + + role := authtypes.SigNozManagedRoleToExistingLegacyRole[userRoles[0].Role.Name] + + return authtypes.NewIdentity(user.ID, orgID, user.Email, role, authtypes.IdentNProviderTokenizer), nil } diff --git a/pkg/identn/impersonationidentn/provider.go b/pkg/identn/impersonationidentn/provider.go index 9fc73a15172..b8f50cb627b 100644 --- a/pkg/identn/impersonationidentn/provider.go +++ b/pkg/identn/impersonationidentn/provider.go @@ -79,16 +79,22 @@ func (provider *provider) GetIdentity(req *http.Request) (*authtypes.Identity, e return nil, err } - rootUser, err := provider.userGetter.GetRootUserByOrgID(ctx, org.ID) + rootUser, userRoles, err := provider.userGetter.GetRootUserByOrgID(ctx, org.ID) if err != nil { return nil, err } + if len(userRoles) == 0 { + return nil, errors.New(errors.TypeUnexpected, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found") + } + + role := authtypes.SigNozManagedRoleToExistingLegacyRole[userRoles[0].Role.Name] + provider.identity = authtypes.NewIdentity( rootUser.ID, rootUser.OrgID, rootUser.Email, - rootUser.Role, + role, authtypes.IdentNProviderImpersonation, ) diff --git a/pkg/modules/session/implsession/module.go b/pkg/modules/session/implsession/module.go index d2e84a6f417..f0d2a011fad 100644 --- a/pkg/modules/session/implsession/module.go +++ b/pkg/modules/session/implsession/module.go @@ -24,18 +24,18 @@ import ( type module struct { settings factory.ScopedProviderSettings authNs map[authtypes.AuthNProvider]authn.AuthN - user user.Module + userSetter user.Setter userGetter user.Getter authDomain authdomain.Module tokenizer tokenizer.Tokenizer orgGetter organization.Getter } -func NewModule(providerSettings factory.ProviderSettings, authNs map[authtypes.AuthNProvider]authn.AuthN, user user.Module, userGetter user.Getter, authDomain authdomain.Module, tokenizer tokenizer.Tokenizer, orgGetter organization.Getter) session.Module { +func NewModule(providerSettings factory.ProviderSettings, authNs map[authtypes.AuthNProvider]authn.AuthN, userSetter user.Setter, userGetter user.Getter, authDomain authdomain.Module, tokenizer tokenizer.Tokenizer, orgGetter organization.Getter) session.Module { return &module{ settings: factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/session/implsession"), authNs: authNs, - user: user, + userSetter: userSetter, userGetter: userGetter, authDomain: authDomain, tokenizer: tokenizer, @@ -144,22 +144,34 @@ func (module *module) CreateCallbackAuthNSession(ctx context.Context, authNProvi roleMapping := authDomain.AuthDomainConfig().RoleMapping role := roleMapping.NewRoleFromCallbackIdentity(callbackIdentity) + signozManagedRole := authtypes.MustGetSigNozManagedRoleFromExistingRole(role) - user, err := types.NewUser(callbackIdentity.Name, callbackIdentity.Email, role, callbackIdentity.OrgID, types.UserStatusActive) + newUser, err := types.NewUser(callbackIdentity.Name, callbackIdentity.Email, callbackIdentity.OrgID, types.UserStatusActive) if err != nil { return "", err } - user, err = module.user.GetOrCreateUser(ctx, user) + newUser, err = module.userSetter.GetOrCreateUser(ctx, newUser, user.WithRoleNames([]string{signozManagedRole})) if err != nil { return "", err } - if err := user.ErrIfRoot(); err != nil { + if err := newUser.ErrIfRoot(); err != nil { return "", errors.WithAdditionalf(err, "root user can only authenticate via password") } - token, err := module.tokenizer.CreateToken(ctx, authtypes.NewIdentity(user.ID, user.OrgID, user.Email, user.Role, authtypes.IdentNProviderTokenizer), map[string]string{}) + userRoles, err := module.userGetter.GetUserRoles(ctx, newUser.ID) + if err != nil { + return "", err + } + + if len(userRoles) == 0 { + return "", errors.New(errors.TypeUnexpected, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found") + } + + finalRole := authtypes.SigNozManagedRoleToExistingLegacyRole[userRoles[0].Role.Name] + + token, err := module.tokenizer.CreateToken(ctx, authtypes.NewIdentity(newUser.ID, newUser.OrgID, newUser.Email, finalRole, authtypes.IdentNProviderTokenizer), map[string]string{}) if err != nil { return "", err } diff --git a/pkg/modules/user/impluser/getter.go b/pkg/modules/user/impluser/getter.go index 807cbea33e8..1a5499443c6 100644 --- a/pkg/modules/user/impluser/getter.go +++ b/pkg/modules/user/impluser/getter.go @@ -4,27 +4,40 @@ import ( "context" "slices" + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/flagger" "github.com/SigNoz/signoz/pkg/modules/user" "github.com/SigNoz/signoz/pkg/types" + "github.com/SigNoz/signoz/pkg/types/authtypes" "github.com/SigNoz/signoz/pkg/types/featuretypes" "github.com/SigNoz/signoz/pkg/valuer" ) type getter struct { - store types.UserStore - flagger flagger.Flagger + store types.UserStore + userRoleStore authtypes.UserRoleStore + flagger flagger.Flagger } -func NewGetter(store types.UserStore, flagger flagger.Flagger) user.Getter { - return &getter{store: store, flagger: flagger} +func NewGetter(store types.UserStore, userRoleStore authtypes.UserRoleStore, flagger flagger.Flagger) user.Getter { + return &getter{store: store, userRoleStore: userRoleStore, flagger: flagger} } -func (module *getter) GetRootUserByOrgID(ctx context.Context, orgID valuer.UUID) (*types.User, error) { - return module.store.GetRootUserByOrgID(ctx, orgID) +func (module *getter) GetRootUserByOrgID(ctx context.Context, orgID valuer.UUID) (*types.User, []*authtypes.UserRole, error) { + rootUser, err := module.store.GetRootUserByOrgID(ctx, orgID) + if err != nil { + return nil, nil, err + } + + userRoles, err := module.userRoleStore.GetUserRolesByUserID(ctx, rootUser.ID) + if err != nil { + return nil, nil, err + } + + return rootUser, userRoles, nil } -func (module *getter) ListByOrgID(ctx context.Context, orgID valuer.UUID) ([]*types.User, error) { +func (module *getter) ListByOrgID(ctx context.Context, orgID valuer.UUID) ([]*types.DeprecatedUser, error) { users, err := module.store.ListUsersByOrgID(ctx, orgID) if err != nil { return nil, err @@ -38,43 +51,81 @@ func (module *getter) ListByOrgID(ctx context.Context, orgID valuer.UUID) ([]*ty users = slices.DeleteFunc(users, func(user *types.User) bool { return user.IsRoot }) } - return users, nil -} + userIDs := make([]valuer.UUID, len(users)) + for idx, user := range users { + userIDs[idx] = user.ID + } -func (module *getter) GetUsersByEmail(ctx context.Context, email valuer.Email) ([]*types.User, error) { - users, err := module.store.GetUsersByEmail(ctx, email) + userRoles, err := module.userRoleStore.ListUserRolesByOrgIDAndUserIDs(ctx, orgID, userIDs) if err != nil { return nil, err } - return users, nil + // Build userID → role name mapping directly from the joined Role + userIDToRoleNames := make(map[valuer.UUID][]string) + for _, ur := range userRoles { + if ur.Role != nil { + userIDToRoleNames[ur.UserID] = append(userIDToRoleNames[ur.UserID], ur.Role.Name) + } + } + + deprecatedUsers := make([]*types.DeprecatedUser, 0, len(users)) + for _, user := range users { + roleNames := userIDToRoleNames[user.ID] + + if len(roleNames) == 0 { + return nil, errors.Newf(errors.TypeUnexpected, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found for user: %s", user.ID.String()) + } + + role := authtypes.SigNozManagedRoleToExistingLegacyRole[roleNames[0]] + deprecatedUsers = append(deprecatedUsers, types.NewDeprecatedUserFromUserAndRole(user, role)) + } + + return deprecatedUsers, nil } -func (module *getter) GetByOrgIDAndID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*types.User, error) { +func (module *getter) GetDeprecatedUserByOrgIDAndID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*types.DeprecatedUser, error) { user, err := module.store.GetByOrgIDAndID(ctx, orgID, id) if err != nil { return nil, err } - return user, nil + userRoles, err := module.GetUserRoles(ctx, id) + if err != nil { + return nil, err + } + + if len(userRoles) == 0 { + return nil, errors.New(errors.TypeUnexpected, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found") + } + + role := authtypes.SigNozManagedRoleToExistingLegacyRole[userRoles[0].Role.Name] + + return types.NewDeprecatedUserFromUserAndRole(user, role), nil } -func (module *getter) Get(ctx context.Context, id valuer.UUID) (*types.User, error) { +func (module *getter) Get(ctx context.Context, id valuer.UUID) (*types.DeprecatedUser, error) { user, err := module.store.GetUser(ctx, id) if err != nil { return nil, err } - return user, nil -} - -func (module *getter) ListUsersByEmailAndOrgIDs(ctx context.Context, email valuer.Email, orgIDs []valuer.UUID) ([]*types.User, error) { - users, err := module.store.ListUsersByEmailAndOrgIDs(ctx, email, orgIDs) + userRoles, err := module.GetUserRoles(ctx, id) if err != nil { return nil, err } - return users, nil + if len(userRoles) == 0 { + return nil, errors.New(errors.TypeUnexpected, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found") + } + + role := authtypes.SigNozManagedRoleToExistingLegacyRole[userRoles[0].Role.Name] + + return types.NewDeprecatedUserFromUserAndRole(user, role), nil +} + +func (module *getter) ListUsersByEmailAndOrgIDs(ctx context.Context, email valuer.Email, orgIDs []valuer.UUID) ([]*types.User, error) { + return module.store.ListUsersByEmailAndOrgIDs(ctx, email, orgIDs) } func (module *getter) CountByOrgID(ctx context.Context, orgID valuer.UUID) (int64, error) { @@ -103,3 +154,31 @@ func (module *getter) GetFactorPasswordByUserID(ctx context.Context, userID valu return factorPassword, nil } + +// this function restricts that only one non-deleted user email can exist for an org ID, if found more, it throws an error +func (module *getter) GetNonDeletedUserByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) (*types.User, error) { + existingUsers, err := module.store.GetNonDeletedUsersByEmailAndOrgID(ctx, email, orgID) + if err != nil { + return nil, err + } + + if len(existingUsers) > 1 { + return nil, errors.Newf(errors.TypeInternal, errors.CodeInternal, "Multiple non-deleted users found for email %s in org_id: %s", email.StringValue(), orgID.StringValue()) + } + + if len(existingUsers) == 1 { + return existingUsers[0], nil + } + + return nil, errors.Newf(errors.TypeNotFound, errors.CodeNotFound, "No non-deleted user found with email %s in org_id: %s", email.StringValue(), orgID.StringValue()) + +} + +func (module *getter) GetUserRoles(ctx context.Context, userID valuer.UUID) ([]*authtypes.UserRole, error) { + userRoles, err := module.userRoleStore.GetUserRolesByUserID(ctx, userID) + if err != nil { + return nil, err + } + + return userRoles, nil +} diff --git a/pkg/modules/user/impluser/handler.go b/pkg/modules/user/impluser/handler.go index a17c7d8563b..0e4fe064971 100644 --- a/pkg/modules/user/impluser/handler.go +++ b/pkg/modules/user/impluser/handler.go @@ -19,12 +19,12 @@ import ( ) type handler struct { - module root.Module + setter root.Setter getter root.Getter } -func NewHandler(module root.Module, getter root.Getter) root.Handler { - return &handler{module: module, getter: getter} +func NewHandler(setter root.Setter, getter root.Getter) root.Handler { + return &handler{setter: setter, getter: getter} } func (h *handler) CreateInvite(rw http.ResponseWriter, r *http.Request) { @@ -43,7 +43,7 @@ func (h *handler) CreateInvite(rw http.ResponseWriter, r *http.Request) { return } - invites, err := h.module.CreateBulkInvite(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(claims.UserID), &types.PostableBulkInviteRequest{ + invites, err := h.setter.CreateBulkInvite(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(claims.UserID), &types.PostableBulkInviteRequest{ Invites: []types.PostableInvite{req}, }) if err != nil { @@ -76,7 +76,7 @@ func (h *handler) CreateBulkInvite(rw http.ResponseWriter, r *http.Request) { return } - _, err = h.module.CreateBulkInvite(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(claims.UserID), &req) + _, err = h.setter.CreateBulkInvite(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(claims.UserID), &req) if err != nil { render.Error(rw, err) return @@ -97,7 +97,7 @@ func (h *handler) GetUser(w http.ResponseWriter, r *http.Request) { return } - user, err := h.getter.GetByOrgIDAndID(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(id)) + user, err := h.getter.GetDeprecatedUserByOrgIDAndID(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(id)) if err != nil { render.Error(w, err) return @@ -116,7 +116,7 @@ func (h *handler) GetMyUser(w http.ResponseWriter, r *http.Request) { return } - user, err := h.getter.GetByOrgIDAndID(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(claims.UserID)) + user, err := h.getter.GetDeprecatedUserByOrgIDAndID(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(claims.UserID)) if err != nil { render.Error(w, err) return @@ -156,13 +156,13 @@ func (h *handler) UpdateUser(w http.ResponseWriter, r *http.Request) { return } - var user types.User + user := types.DeprecatedUser{User: &types.User{}} if err := json.NewDecoder(r.Body).Decode(&user); err != nil { render.Error(w, err) return } - updatedUser, err := h.module.UpdateUser(ctx, valuer.MustNewUUID(claims.OrgID), id, &user, claims.UserID) + updatedUser, err := h.setter.UpdateUser(ctx, valuer.MustNewUUID(claims.OrgID), id, &user, claims.UserID) if err != nil { render.Error(w, err) return @@ -183,7 +183,7 @@ func (h *handler) DeleteUser(w http.ResponseWriter, r *http.Request) { return } - if err := h.module.DeleteUser(ctx, valuer.MustNewUUID(claims.OrgID), id, claims.UserID); err != nil { + if err := h.setter.DeleteUser(ctx, valuer.MustNewUUID(claims.OrgID), id, claims.UserID); err != nil { render.Error(w, err) return } @@ -203,13 +203,13 @@ func (handler *handler) GetResetPasswordToken(w http.ResponseWriter, r *http.Req return } - user, err := handler.getter.GetByOrgIDAndID(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(id)) + user, err := handler.getter.GetDeprecatedUserByOrgIDAndID(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(id)) if err != nil { render.Error(w, err) return } - token, err := handler.module.GetOrCreateResetPasswordToken(ctx, user.ID) + token, err := handler.setter.GetOrCreateResetPasswordToken(ctx, user.ID) if err != nil { render.Error(w, err) return @@ -228,7 +228,7 @@ func (handler *handler) ResetPassword(w http.ResponseWriter, r *http.Request) { return } - err := handler.module.UpdatePasswordByResetPasswordToken(ctx, req.Token, req.Password) + err := handler.setter.UpdatePasswordByResetPasswordToken(ctx, req.Token, req.Password) if err != nil { render.Error(w, err) return @@ -247,7 +247,7 @@ func (handler *handler) ChangePassword(w http.ResponseWriter, r *http.Request) { return } - err := handler.module.UpdatePassword(ctx, req.UserID, req.OldPassword, req.NewPassword) + err := handler.setter.UpdatePassword(ctx, req.UserID, req.OldPassword, req.NewPassword) if err != nil { render.Error(w, err) return @@ -266,7 +266,7 @@ func (h *handler) ForgotPassword(w http.ResponseWriter, r *http.Request) { return } - err := h.module.ForgotPassword(ctx, req.OrgID, req.Email, req.FrontendBaseURL) + err := h.setter.ForgotPassword(ctx, req.OrgID, req.Email, req.FrontendBaseURL) if err != nil { render.Error(w, err) return @@ -302,13 +302,13 @@ func (h *handler) CreateAPIKey(w http.ResponseWriter, r *http.Request) { return } - err = h.module.CreateAPIKey(ctx, apiKey) + err = h.setter.CreateAPIKey(ctx, apiKey) if err != nil { render.Error(w, err) return } - createdApiKey, err := h.module.GetAPIKey(ctx, valuer.MustNewUUID(claims.OrgID), apiKey.ID) + createdApiKey, err := h.setter.GetAPIKey(ctx, valuer.MustNewUUID(claims.OrgID), apiKey.ID) if err != nil { render.Error(w, err) return @@ -328,7 +328,7 @@ func (h *handler) ListAPIKeys(w http.ResponseWriter, r *http.Request) { return } - apiKeys, err := h.module.ListAPIKeys(ctx, valuer.MustNewUUID(claims.OrgID)) + apiKeys, err := h.setter.ListAPIKeys(ctx, valuer.MustNewUUID(claims.OrgID)) if err != nil { render.Error(w, err) return @@ -373,7 +373,7 @@ func (h *handler) UpdateAPIKey(w http.ResponseWriter, r *http.Request) { } //get the API Key - existingAPIKey, err := h.module.GetAPIKey(ctx, valuer.MustNewUUID(claims.OrgID), id) + existingAPIKey, err := h.setter.GetAPIKey(ctx, valuer.MustNewUUID(claims.OrgID), id) if err != nil { render.Error(w, err) return @@ -391,7 +391,7 @@ func (h *handler) UpdateAPIKey(w http.ResponseWriter, r *http.Request) { return } - err = h.module.UpdateAPIKey(ctx, id, &req, valuer.MustNewUUID(claims.UserID)) + err = h.setter.UpdateAPIKey(ctx, id, &req, valuer.MustNewUUID(claims.UserID)) if err != nil { render.Error(w, err) return @@ -418,7 +418,7 @@ func (h *handler) RevokeAPIKey(w http.ResponseWriter, r *http.Request) { } //get the API Key - existingAPIKey, err := h.module.GetAPIKey(ctx, valuer.MustNewUUID(claims.OrgID), id) + existingAPIKey, err := h.setter.GetAPIKey(ctx, valuer.MustNewUUID(claims.OrgID), id) if err != nil { render.Error(w, err) return @@ -436,7 +436,7 @@ func (h *handler) RevokeAPIKey(w http.ResponseWriter, r *http.Request) { return } - if err := h.module.RevokeAPIKey(ctx, id, valuer.MustNewUUID(claims.UserID)); err != nil { + if err := h.setter.RevokeAPIKey(ctx, id, valuer.MustNewUUID(claims.UserID)); err != nil { render.Error(w, err) return } diff --git a/pkg/modules/user/impluser/service.go b/pkg/modules/user/impluser/service.go index 60a50eb8ba1..c98168ee5af 100644 --- a/pkg/modules/user/impluser/service.go +++ b/pkg/modules/user/impluser/service.go @@ -17,7 +17,8 @@ import ( type service struct { settings factory.ScopedProviderSettings store types.UserStore - module user.Module + getter user.Getter + setter user.Setter orgGetter organization.Getter authz authz.AuthZ config user.RootConfig @@ -27,7 +28,8 @@ type service struct { func NewService( providerSettings factory.ProviderSettings, store types.UserStore, - module user.Module, + getter user.Getter, + setter user.Setter, orgGetter organization.Getter, authz authz.AuthZ, config user.RootConfig, @@ -35,7 +37,8 @@ func NewService( return &service{ settings: factory.NewScopedProviderSettings(providerSettings, "go.signoz.io/pkg/modules/user"), store: store, - module: module, + getter: getter, + setter: setter, orgGetter: orgGetter, authz: authz, config: config, @@ -85,12 +88,12 @@ func (s *service) reconcile(ctx context.Context) error { if s.config.Org.ID.IsZero() { newOrg := types.NewOrganization(s.config.Org.Name, s.config.Org.Name) - _, err := s.module.CreateFirstUser(ctx, newOrg, s.config.Email.String(), s.config.Email, s.config.Password) + _, err := s.setter.CreateFirstUser(ctx, newOrg, s.config.Email.String(), s.config.Email, s.config.Password) return err } newOrg := types.NewOrganizationWithID(s.config.Org.ID, s.config.Org.Name, s.config.Org.Name) - _, err = s.module.CreateFirstUser(ctx, newOrg, s.config.Email.String(), s.config.Email, s.config.Password) + _, err = s.setter.CreateFirstUser(ctx, newOrg, s.config.Email.String(), s.config.Email, s.config.Password) return err } @@ -103,44 +106,72 @@ func (s *service) reconcile(ctx context.Context) error { } func (s *service) reconcileRootUser(ctx context.Context, orgID valuer.UUID) error { - existingRoot, err := s.store.GetRootUserByOrgID(ctx, orgID) + existingStorableRoot, err := s.store.GetRootUserByOrgID(ctx, orgID) if err != nil && !errors.Ast(err, errors.TypeNotFound) { return err } - if existingRoot == nil { + if existingStorableRoot == nil { return s.createOrPromoteRootUser(ctx, orgID) } - return s.updateExistingRootUser(ctx, orgID, existingRoot) + return s.updateExistingRootUser(ctx, orgID, existingStorableRoot) } func (s *service) createOrPromoteRootUser(ctx context.Context, orgID valuer.UUID) error { - existingUser, err := s.module.GetNonDeletedUserByEmailAndOrgID(ctx, s.config.Email, orgID) + existingUser, err := s.getter.GetNonDeletedUserByEmailAndOrgID(ctx, s.config.Email, orgID) if err != nil && !errors.Ast(err, errors.TypeNotFound) { return err } if existingUser != nil { - oldRole := existingUser.Role + userRoles, err := s.getter.GetUserRoles(ctx, existingUser.ID) + if err != nil { + return err + } - existingUser.PromoteToRoot() - if err := s.module.UpdateAnyUser(ctx, orgID, existingUser); err != nil { + existingUserRoleNames := make([]string, len(userRoles)) + for idx, userRole := range userRoles { + existingUserRoleNames[idx] = userRole.Role.Name + } + + // idempotent - safe to retry can't put this in a txn + if err := s.authz.ModifyGrant(ctx, + orgID, + existingUserRoleNames, + []string{authtypes.SigNozAdminRoleName}, + authtypes.MustNewSubject(authtypes.TypeableUser, existingUser.ID.StringValue(), orgID, nil), + ); err != nil { return err } - if oldRole != types.RoleAdmin { - if err := s.authz.ModifyGrant(ctx, - orgID, - []string{authtypes.MustGetSigNozManagedRoleFromExistingRole(oldRole)}, - []string{authtypes.MustGetSigNozManagedRoleFromExistingRole(types.RoleAdmin)}, - authtypes.MustNewSubject(authtypes.TypeableUser, existingUser.ID.StringValue(), orgID, nil), + existingUser.PromoteToRoot() + + err = s.store.RunInTx(ctx, func(ctx context.Context) error { + // update users table + deprecatedUser := types.NewDeprecatedUserFromUserAndRole(existingUser, types.RoleAdmin) + if err := s.setter.UpdateAnyUser(ctx, orgID, deprecatedUser); err != nil { + return err + } + + // update user_role entries + if err := s.setter.UpdateUserRoles( + ctx, + existingUser.OrgID, + existingUser.ID, + []string{authtypes.SigNozAdminRoleName}, ); err != nil { return err } + + // set password + return s.setPassword(ctx, existingUser.ID) + }) + if err != nil { + return err } - return s.setPassword(ctx, existingUser.ID) + return nil } // Create new root user @@ -154,7 +185,7 @@ func (s *service) createOrPromoteRootUser(ctx context.Context, orgID valuer.UUID return err } - return s.module.CreateUser(ctx, newUser, user.WithFactorPassword(factorPassword)) + return s.setter.CreateUser(ctx, newUser, user.WithFactorPassword(factorPassword), user.WithRoleNames([]string{authtypes.SigNozAdminRoleName})) } func (s *service) updateExistingRootUser(ctx context.Context, orgID valuer.UUID, existingRoot *types.User) error { @@ -162,7 +193,8 @@ func (s *service) updateExistingRootUser(ctx context.Context, orgID valuer.UUID, if existingRoot.Email != s.config.Email { existingRoot.UpdateEmail(s.config.Email) - if err := s.module.UpdateAnyUser(ctx, orgID, existingRoot); err != nil { + deprecatedUser := types.NewDeprecatedUserFromUserAndRole(existingRoot, types.RoleAdmin) + if err := s.setter.UpdateAnyUser(ctx, orgID, deprecatedUser); err != nil { return err } } diff --git a/pkg/modules/user/impluser/module.go b/pkg/modules/user/impluser/setter.go similarity index 60% rename from pkg/modules/user/impluser/module.go rename to pkg/modules/user/impluser/setter.go index a1aed885d1c..c400d2ac546 100644 --- a/pkg/modules/user/impluser/module.go +++ b/pkg/modules/user/impluser/setter.go @@ -25,35 +25,39 @@ import ( "github.com/SigNoz/signoz/pkg/valuer" ) -type Module struct { - store types.UserStore - tokenizer tokenizer.Tokenizer - emailing emailing.Emailing - settings factory.ScopedProviderSettings - orgSetter organization.Setter - authz authz.AuthZ - analytics analytics.Analytics - config user.Config +type setter struct { + store types.UserStore + userRoleStore authtypes.UserRoleStore + tokenizer tokenizer.Tokenizer + emailing emailing.Emailing + settings factory.ScopedProviderSettings + orgSetter organization.Setter + authz authz.AuthZ + analytics analytics.Analytics + config user.Config + getter root.Getter } // This module is a WIP, don't take inspiration from this. -func NewModule(store types.UserStore, tokenizer tokenizer.Tokenizer, emailing emailing.Emailing, providerSettings factory.ProviderSettings, orgSetter organization.Setter, authz authz.AuthZ, analytics analytics.Analytics, config user.Config) root.Module { +func NewSetter(store types.UserStore, tokenizer tokenizer.Tokenizer, emailing emailing.Emailing, providerSettings factory.ProviderSettings, orgSetter organization.Setter, authz authz.AuthZ, analytics analytics.Analytics, config user.Config, userRoleStore authtypes.UserRoleStore, getter root.Getter) root.Setter { settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/user/impluser") - return &Module{ - store: store, - tokenizer: tokenizer, - emailing: emailing, - settings: settings, - orgSetter: orgSetter, - analytics: analytics, - authz: authz, - config: config, + return &setter{ + store: store, + userRoleStore: userRoleStore, + tokenizer: tokenizer, + emailing: emailing, + settings: settings, + orgSetter: orgSetter, + analytics: analytics, + authz: authz, + config: config, + getter: getter, } } // CreateBulk implements invite.Module. -func (m *Module) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, bulkInvites *types.PostableBulkInviteRequest) ([]*types.Invite, error) { - creator, err := m.store.GetUser(ctx, userID) +func (module *setter) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, bulkInvites *types.PostableBulkInviteRequest) ([]*types.Invite, error) { + creator, err := module.store.GetUser(ctx, userID) if err != nil { return nil, err } @@ -63,7 +67,7 @@ func (m *Module) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID for idx, invite := range bulkInvites.Invites { emails[idx] = invite.Email.StringValue() } - users, err := m.store.GetUsersByEmailsOrgIDAndStatuses(ctx, orgID, emails, []string{types.UserStatusActive.StringValue(), types.UserStatusPendingInvite.StringValue()}) + users, err := module.store.GetUsersByEmailsOrgIDAndStatuses(ctx, orgID, emails, []string{types.UserStatusActive.StringValue(), types.UserStatusPendingInvite.StringValue()}) if err != nil { return nil, err } @@ -83,39 +87,36 @@ func (m *Module) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID type userWithResetToken struct { User *types.User ResetPasswordToken *types.ResetPasswordToken + Role types.Role } newUsersWithResetToken := make([]*userWithResetToken, len(bulkInvites.Invites)) - if err := m.store.RunInTx(ctx, func(ctx context.Context) error { + if err := module.store.RunInTx(ctx, func(ctx context.Context) error { for idx, invite := range bulkInvites.Invites { - role, err := types.NewRole(invite.Role.String()) - if err != nil { - return err - } - // create a new user with pending invite status - newUser, err := types.NewUser(invite.Name, invite.Email, role, orgID, types.UserStatusPendingInvite) + newUser, err := types.NewUser(invite.Name, invite.Email, orgID, types.UserStatusPendingInvite) if err != nil { return err } // store the user and password in db - err = m.createUserWithoutGrant(ctx, newUser) + err = module.createUserWithoutGrant(ctx, newUser, root.WithRoleNames([]string{authtypes.MustGetSigNozManagedRoleFromExistingRole(invite.Role)})) if err != nil { return err } // generate reset password token - resetPasswordToken, err := m.GetOrCreateResetPasswordToken(ctx, newUser.ID) + resetPasswordToken, err := module.GetOrCreateResetPasswordToken(ctx, newUser.ID) if err != nil { - m.settings.Logger().ErrorContext(ctx, "failed to create reset password token for invited user", errors.Attr(err)) + module.settings.Logger().ErrorContext(ctx, "failed to create reset password token for invited user", errors.Attr(err)) return err } newUsersWithResetToken[idx] = &userWithResetToken{ User: newUser, ResetPasswordToken: resetPasswordToken, + Role: invite.Role, } } return nil @@ -127,9 +128,9 @@ func (m *Module) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID // send password reset emails to all the invited users for idx, userWithToken := range newUsersWithResetToken { - m.analytics.TrackUser(ctx, orgID.String(), creator.ID.String(), "Invite Sent", map[string]any{ + module.analytics.TrackUser(ctx, orgID.String(), creator.ID.String(), "Invite Sent", map[string]any{ "invitee_email": userWithToken.User.Email, - "invitee_role": userWithToken.User.Role, + "invitee_role": userWithToken.Role, }) invite := &types.Invite{ @@ -139,7 +140,7 @@ func (m *Module) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID Name: userWithToken.User.DisplayName, Email: userWithToken.User.Email, Token: userWithToken.ResetPasswordToken.Token, - Role: userWithToken.User.Role, + Role: userWithToken.Role, OrgID: userWithToken.User.OrgID, TimeAuditable: types.TimeAuditable{ CreatedAt: userWithToken.User.CreatedAt, @@ -151,38 +152,45 @@ func (m *Module) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID frontendBaseUrl := bulkInvites.Invites[idx].FrontendBaseUrl if frontendBaseUrl == "" { - m.settings.Logger().InfoContext(ctx, "frontend base url is not provided, skipping email", slog.Any("invitee_email", userWithToken.User.Email)) + module.settings.Logger().InfoContext(ctx, "frontend base url is not provided, skipping email", slog.Any("invitee_email", userWithToken.User.Email)) continue } resetLink := userWithToken.ResetPasswordToken.FactorPasswordResetLink(frontendBaseUrl) - tokenLifetime := m.config.Password.Invite.MaxTokenLifetime + tokenLifetime := module.config.Password.Invite.MaxTokenLifetime humanizedTokenLifetime := strings.TrimSpace(humanize.RelTime(time.Now(), time.Now().Add(tokenLifetime), "", "")) - if err := m.emailing.SendHTML(ctx, userWithToken.User.Email.String(), "You're Invited to Join SigNoz", emailtypes.TemplateNameInvitationEmail, map[string]any{ + if err := module.emailing.SendHTML(ctx, userWithToken.User.Email.String(), "You're Invited to Join SigNoz", emailtypes.TemplateNameInvitationEmail, map[string]any{ "inviter_email": creator.Email, "link": resetLink, "Expiry": humanizedTokenLifetime, }); err != nil { - m.settings.Logger().ErrorContext(ctx, "failed to send invite email", errors.Attr(err)) + module.settings.Logger().ErrorContext(ctx, "failed to send invite email", errors.Attr(err)) } } return invites, nil } -func (module *Module) CreateUser(ctx context.Context, input *types.User, opts ...root.CreateUserOption) error { +func (module *setter) CreateUser(ctx context.Context, user *types.User, opts ...root.CreateUserOption) error { createUserOpts := root.NewCreateUserOptions(opts...) // since assign is idempotant multiple calls to assign won't cause issues in case of retries. - err := module.authz.Grant(ctx, input.OrgID, []string{authtypes.MustGetSigNozManagedRoleFromExistingRole(input.Role)}, authtypes.MustNewSubject(authtypes.TypeableUser, input.ID.StringValue(), input.OrgID, nil)) - if err != nil { - return err + if len(createUserOpts.RoleNames) > 0 { + err := module.authz.Grant( + ctx, + user.OrgID, + createUserOpts.RoleNames, + authtypes.MustNewSubject(authtypes.TypeableUser, user.ID.StringValue(), user.OrgID, nil), + ) + if err != nil { + return err + } } if err := module.store.RunInTx(ctx, func(ctx context.Context) error { - if err := module.store.CreateUser(ctx, input); err != nil { + if err := module.store.CreateUser(ctx, user); err != nil { return err } @@ -192,20 +200,28 @@ func (module *Module) CreateUser(ctx context.Context, input *types.User, opts .. } } + // create user_role entries + if len(createUserOpts.RoleNames) > 0 { + err := module.createUserRoleEntries(ctx, user.OrgID, user.ID, createUserOpts.RoleNames) + if err != nil { + return err + } + } + return nil }); err != nil { return err } - traitsOrProperties := types.NewTraitsFromUser(input) - module.analytics.IdentifyUser(ctx, input.OrgID.String(), input.ID.String(), traitsOrProperties) - module.analytics.TrackUser(ctx, input.OrgID.String(), input.ID.String(), "User Created", traitsOrProperties) + traitsOrProperties := types.NewTraitsFromUser(user) + module.analytics.IdentifyUser(ctx, user.OrgID.String(), user.ID.String(), traitsOrProperties) + module.analytics.TrackUser(ctx, user.OrgID.String(), user.ID.String(), "User Created", traitsOrProperties) return nil } -func (m *Module) UpdateUser(ctx context.Context, orgID valuer.UUID, id string, user *types.User, updatedBy string) (*types.User, error) { - existingUser, err := m.store.GetUser(ctx, valuer.MustNewUUID(id)) +func (module *setter) UpdateUser(ctx context.Context, orgID valuer.UUID, id string, user *types.DeprecatedUser, updatedBy string) (*types.DeprecatedUser, error) { + existingUser, err := module.getter.GetDeprecatedUserByOrgIDAndID(ctx, orgID, valuer.MustNewUUID(id)) if err != nil { return nil, err } @@ -218,29 +234,24 @@ func (m *Module) UpdateUser(ctx context.Context, orgID valuer.UUID, id string, u return nil, errors.WithAdditionalf(err, "cannot update deleted user") } - requestor, err := m.store.GetUser(ctx, valuer.MustNewUUID(updatedBy)) + requestor, err := module.getter.GetDeprecatedUserByOrgIDAndID(ctx, orgID, valuer.MustNewUUID(updatedBy)) if err != nil { return nil, err } - if user.Role != "" && user.Role != existingUser.Role && requestor.Role != types.RoleAdmin { + roleChange := user.Role != "" && user.Role != existingUser.Role + + if roleChange && requestor.Role != types.RoleAdmin { return nil, errors.New(errors.TypeForbidden, errors.CodeForbidden, "only admins can change roles") } - // Make sure that the request is not demoting the last admin user. - if user.Role != "" && user.Role != existingUser.Role && existingUser.Role == types.RoleAdmin { - adminUsers, err := m.store.GetActiveUsersByRoleAndOrgID(ctx, types.RoleAdmin, orgID) - if err != nil { - return nil, err - } - - if len(adminUsers) == 1 { - return nil, errors.New(errors.TypeForbidden, errors.CodeForbidden, "cannot demote the last admin") - } + // make sure the user is not demoting self from admin + if roleChange && existingUser.ID == requestor.ID && existingUser.Role == types.RoleAdmin && user.Role != types.RoleAdmin { + return nil, errors.New(errors.TypeForbidden, errors.CodeForbidden, "cannot change self role") } - if user.Role != "" && user.Role != existingUser.Role { - err = m.authz.ModifyGrant(ctx, + if roleChange { + err = module.authz.ModifyGrant(ctx, orgID, []string{authtypes.MustGetSigNozManagedRoleFromExistingRole(existingUser.Role)}, []string{authtypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)}, @@ -252,19 +263,41 @@ func (m *Module) UpdateUser(ctx context.Context, orgID valuer.UUID, id string, u } existingUser.Update(user.DisplayName, user.Role) - if err := m.UpdateAnyUser(ctx, orgID, existingUser); err != nil { + + // update the user - idempotent (this does analytics too so keeping it outside txn) + if err := module.UpdateAnyUser(ctx, orgID, existingUser); err != nil { + return nil, err + } + + err = module.store.RunInTx(ctx, func(ctx context.Context) error { + if roleChange { + // delete old role entries and create new ones + if err := module.userRoleStore.DeleteUserRoles(ctx, existingUser.ID); err != nil { + return err + } + + // create new ones + if err := module.createUserRoleEntries(ctx, existingUser.OrgID, existingUser.ID, []string{authtypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)}); err != nil { + return err + } + } + + return nil + }) + if err != nil { return nil, err } return existingUser, nil } -func (module *Module) UpdateAnyUser(ctx context.Context, orgID valuer.UUID, user *types.User) error { +func (module *setter) UpdateAnyUser(ctx context.Context, orgID valuer.UUID, deprecateUser *types.DeprecatedUser) error { + user := types.NewUserFromDeprecatedUser(deprecateUser) if err := module.store.UpdateUser(ctx, orgID, user); err != nil { return err } - traits := types.NewTraitsFromUser(user) + traits := types.NewTraitsFromDeprecatedUser(deprecateUser) module.analytics.IdentifyUser(ctx, user.OrgID.String(), user.ID.String(), traits) module.analytics.TrackUser(ctx, user.OrgID.String(), user.ID.String(), "User Updated", traits) @@ -275,7 +308,7 @@ func (module *Module) UpdateAnyUser(ctx context.Context, orgID valuer.UUID, user return nil } -func (module *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id string, deletedBy string) error { +func (module *setter) DeleteUser(ctx context.Context, orgID valuer.UUID, id string, deletedBy string) error { user, err := module.store.GetUser(ctx, valuer.MustNewUUID(id)) if err != nil { return err @@ -293,18 +326,29 @@ func (module *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id stri return errors.New(errors.TypeForbidden, errors.CodeForbidden, "integration user cannot be deleted") } - // don't allow to delete the last admin user - adminUsers, err := module.store.GetActiveUsersByRoleAndOrgID(ctx, types.RoleAdmin, orgID) + deleter, err := module.store.GetUser(ctx, valuer.MustNewUUID(deletedBy)) if err != nil { return err } - if len(adminUsers) == 1 && user.Role == types.RoleAdmin { - return errors.New(errors.TypeForbidden, errors.CodeForbidden, "cannot delete the last admin") + if deleter.ID == user.ID { + return errors.New(errors.TypeForbidden, errors.CodeForbidden, "cannot self delete") + } + + userRoles, err := module.getter.GetUserRoles(ctx, user.ID) + if err != nil { + return err } + roleNames := roleNamesFromUserRoles(userRoles) + // since revoke is idempotant multiple calls to revoke won't cause issues in case of retries - err = module.authz.Revoke(ctx, orgID, []string{authtypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)}, authtypes.MustNewSubject(authtypes.TypeableUser, id, orgID, nil)) + err = module.authz.Revoke( + ctx, + orgID, + roleNames, + authtypes.MustNewSubject(authtypes.TypeableUser, id, orgID, nil), + ) if err != nil { return err } @@ -321,7 +365,7 @@ func (module *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id stri return nil } -func (module *Module) GetOrCreateResetPasswordToken(ctx context.Context, userID valuer.UUID) (*types.ResetPasswordToken, error) { +func (module *setter) GetOrCreateResetPasswordToken(ctx context.Context, userID valuer.UUID) (*types.ResetPasswordToken, error) { user, err := module.store.GetUser(ctx, userID) if err != nil { return nil, err @@ -388,12 +432,12 @@ func (module *Module) GetOrCreateResetPasswordToken(ctx context.Context, userID return resetPasswordToken, nil } -func (module *Module) ForgotPassword(ctx context.Context, orgID valuer.UUID, email valuer.Email, frontendBaseURL string) error { +func (module *setter) ForgotPassword(ctx context.Context, orgID valuer.UUID, email valuer.Email, frontendBaseURL string) error { if !module.config.Password.Reset.AllowSelf { return errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "Users are not allowed to reset their password themselves, please contact an admin to reset your password.") } - user, err := module.GetNonDeletedUserByEmailAndOrgID(ctx, email, orgID) + user, err := module.getter.GetNonDeletedUserByEmailAndOrgID(ctx, email, orgID) if err != nil { if errors.Ast(err, errors.TypeNotFound) { return nil // for security reasons @@ -436,7 +480,7 @@ func (module *Module) ForgotPassword(ctx context.Context, orgID valuer.UUID, ema return nil } -func (module *Module) UpdatePasswordByResetPasswordToken(ctx context.Context, token string, passwd string) error { +func (module *setter) UpdatePasswordByResetPasswordToken(ctx context.Context, token string, passwd string) error { resetPasswordToken, err := module.store.GetResetPasswordToken(ctx, token) if err != nil { return err @@ -469,16 +513,26 @@ func (module *Module) UpdatePasswordByResetPasswordToken(ctx context.Context, to return err } + userRoles, err := module.getter.GetUserRoles(ctx, user.ID) + if err != nil { + return err + } + + roleNames := roleNamesFromUserRoles(userRoles) + // since grant is idempotent, multiple calls won't cause issues in case of retries if user.Status == types.UserStatusPendingInvite { if err = module.authz.Grant( ctx, user.OrgID, - []string{authtypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)}, + roleNames, authtypes.MustNewSubject(authtypes.TypeableUser, user.ID.StringValue(), user.OrgID, nil), ); err != nil { return err } + + traitsOrProperties := types.NewTraitsFromUser(user) + module.analytics.TrackUser(ctx, user.OrgID.String(), user.ID.String(), "User Activated", traitsOrProperties) } return module.store.RunInTx(ctx, func(ctx context.Context) error { @@ -503,7 +557,7 @@ func (module *Module) UpdatePasswordByResetPasswordToken(ctx context.Context, to }) } -func (module *Module) UpdatePassword(ctx context.Context, userID valuer.UUID, oldpasswd string, passwd string) error { +func (module *setter) UpdatePassword(ctx context.Context, userID valuer.UUID, oldpasswd string, passwd string) error { user, err := module.store.GetUser(ctx, userID) if err != nil { return err @@ -547,8 +601,10 @@ func (module *Module) UpdatePassword(ctx context.Context, userID valuer.UUID, ol return module.tokenizer.DeleteTokensByUserID(ctx, userID) } -func (module *Module) GetOrCreateUser(ctx context.Context, user *types.User, opts ...root.CreateUserOption) (*types.User, error) { - existingUser, err := module.GetNonDeletedUserByEmailAndOrgID(ctx, user.Email, user.OrgID) +func (module *setter) GetOrCreateUser(ctx context.Context, user *types.User, opts ...root.CreateUserOption) (*types.User, error) { + createUserOpts := root.NewCreateUserOptions(opts...) + + existingUser, err := module.getter.GetNonDeletedUserByEmailAndOrgID(ctx, user.Email, user.OrgID) if err != nil { if !errors.Ast(err, errors.TypeNotFound) { return nil, err @@ -556,12 +612,8 @@ func (module *Module) GetOrCreateUser(ctx context.Context, user *types.User, opt } if existingUser != nil { - // for users logging through SSO flow but are having status as pending_invite if existingUser.Status == types.UserStatusPendingInvite { - // respect the role coming from the SSO - existingUser.Update("", user.Role) - // activate the user - if err = module.activatePendingUser(ctx, existingUser); err != nil { + if err = module.activatePendingUser(ctx, existingUser, root.WithRoleNames(createUserOpts.RoleNames)); err != nil { return nil, err } } @@ -569,35 +621,34 @@ func (module *Module) GetOrCreateUser(ctx context.Context, user *types.User, opt return existingUser, nil } - err = module.CreateUser(ctx, user, opts...) - if err != nil { + if err := module.CreateUser(ctx, user, opts...); err != nil { return nil, err } return user, nil } -func (m *Module) CreateAPIKey(ctx context.Context, apiKey *types.StorableAPIKey) error { - return m.store.CreateAPIKey(ctx, apiKey) +func (module *setter) CreateAPIKey(ctx context.Context, apiKey *types.StorableAPIKey) error { + return module.store.CreateAPIKey(ctx, apiKey) } -func (m *Module) UpdateAPIKey(ctx context.Context, id valuer.UUID, apiKey *types.StorableAPIKey, updaterID valuer.UUID) error { - return m.store.UpdateAPIKey(ctx, id, apiKey, updaterID) +func (module *setter) UpdateAPIKey(ctx context.Context, id valuer.UUID, apiKey *types.StorableAPIKey, updaterID valuer.UUID) error { + return module.store.UpdateAPIKey(ctx, id, apiKey, updaterID) } -func (m *Module) ListAPIKeys(ctx context.Context, orgID valuer.UUID) ([]*types.StorableAPIKeyUser, error) { - return m.store.ListAPIKeys(ctx, orgID) +func (module *setter) ListAPIKeys(ctx context.Context, orgID valuer.UUID) ([]*types.StorableAPIKeyUser, error) { + return module.store.ListAPIKeys(ctx, orgID) } -func (m *Module) GetAPIKey(ctx context.Context, orgID, id valuer.UUID) (*types.StorableAPIKeyUser, error) { - return m.store.GetAPIKey(ctx, orgID, id) +func (module *setter) GetAPIKey(ctx context.Context, orgID, id valuer.UUID) (*types.StorableAPIKeyUser, error) { + return module.store.GetAPIKey(ctx, orgID, id) } -func (m *Module) RevokeAPIKey(ctx context.Context, id, removedByUserID valuer.UUID) error { - return m.store.RevokeAPIKey(ctx, id, removedByUserID) +func (module *setter) RevokeAPIKey(ctx context.Context, id, removedByUserID valuer.UUID) error { + return module.store.RevokeAPIKey(ctx, id, removedByUserID) } -func (module *Module) CreateFirstUser(ctx context.Context, organization *types.Organization, name string, email valuer.Email, passwd string) (*types.User, error) { +func (module *setter) CreateFirstUser(ctx context.Context, organization *types.Organization, name string, email valuer.Email, passwd string) (*types.User, error) { user, err := types.NewRootUser(name, email, organization.ID) if err != nil { return nil, err @@ -614,6 +665,8 @@ func (module *Module) CreateFirstUser(ctx context.Context, organization *types.O return nil, err } + roleNames := []string{authtypes.SigNozAdminRoleName} + if err = module.store.RunInTx(ctx, func(ctx context.Context) error { err = module.orgSetter.Create(ctx, organization, func(ctx context.Context, orgID valuer.UUID) error { err = module.authz.CreateManagedRoles(ctx, orgID, managedRoles) @@ -627,7 +680,7 @@ func (module *Module) CreateFirstUser(ctx context.Context, organization *types.O return err } - err = module.createUserWithoutGrant(ctx, user, root.WithFactorPassword(password)) + err = module.CreateUser(ctx, user, root.WithFactorPassword(password), root.WithRoleNames(roleNames)) if err != nil { return err } @@ -640,7 +693,7 @@ func (module *Module) CreateFirstUser(ctx context.Context, organization *types.O return user, nil } -func (module *Module) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) { +func (module *setter) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) { stats := make(map[string]any) counts, err := module.store.CountByOrgIDAndStatuses(ctx, orgID, []string{types.UserStatusActive.StringValue(), types.UserStatusDeleted.StringValue(), types.UserStatusPendingInvite.StringValue()}) if err == nil { @@ -658,32 +711,10 @@ func (module *Module) Collect(ctx context.Context, orgID valuer.UUID) (map[strin return stats, nil } -// this function restricts that only one non-deleted user email can exist for an org ID, if found more, it throws an error -func (module *Module) GetNonDeletedUserByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) (*types.User, error) { - existingUsers, err := module.store.GetUsersByEmailAndOrgID(ctx, email, orgID) - if err != nil { - return nil, err - } - - // filter out the deleted users - existingUsers = slices.DeleteFunc(existingUsers, func(user *types.User) bool { return user.ErrIfDeleted() != nil }) - - if len(existingUsers) > 1 { - return nil, errors.Newf(errors.TypeInternal, errors.CodeInternal, "Multiple non-deleted users found for email %s in org_id: %s", email.StringValue(), orgID.StringValue()) - } - - if len(existingUsers) == 1 { - return existingUsers[0], nil - } - - return nil, errors.Newf(errors.TypeNotFound, errors.CodeNotFound, "No non-deleted user found with email %s in org_id: %s", email.StringValue(), orgID.StringValue()) - -} - -func (module *Module) createUserWithoutGrant(ctx context.Context, input *types.User, opts ...root.CreateUserOption) error { +func (module *setter) createUserWithoutGrant(ctx context.Context, user *types.User, opts ...root.CreateUserOption) error { createUserOpts := root.NewCreateUserOptions(opts...) if err := module.store.RunInTx(ctx, func(ctx context.Context) error { - if err := module.store.CreateUser(ctx, input); err != nil { + if err := module.store.CreateUser(ctx, user); err != nil { return err } @@ -693,36 +724,99 @@ func (module *Module) createUserWithoutGrant(ctx context.Context, input *types.U } } + // create user_role entries + if len(createUserOpts.RoleNames) > 0 { + err := module.createUserRoleEntries(ctx, user.OrgID, user.ID, createUserOpts.RoleNames) + if err != nil { + return err + } + } + return nil }); err != nil { return err } - traitsOrProperties := types.NewTraitsFromUser(input) - module.analytics.IdentifyUser(ctx, input.OrgID.String(), input.ID.String(), traitsOrProperties) - module.analytics.TrackUser(ctx, input.OrgID.String(), input.ID.String(), "User Created", traitsOrProperties) + traitsOrProperties := types.NewTraitsFromUser(user) + module.analytics.IdentifyUser(ctx, user.OrgID.String(), user.ID.String(), traitsOrProperties) + module.analytics.TrackUser(ctx, user.OrgID.String(), user.ID.String(), "User Created", traitsOrProperties) return nil } -func (module *Module) activatePendingUser(ctx context.Context, user *types.User) error { - err := module.authz.Grant( - ctx, - user.OrgID, - []string{authtypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)}, - authtypes.MustNewSubject(authtypes.TypeableUser, user.ID.StringValue(), user.OrgID, nil), - ) +func (module *setter) createUserRoleEntries(ctx context.Context, orgID, userId valuer.UUID, roleNames []string) error { + roles, err := module.authz.ListByOrgIDAndNames(ctx, orgID, roleNames) if err != nil { return err } + userRoles := authtypes.NewUserRoles(userId, roles) + return module.userRoleStore.CreateUserRoles(ctx, userRoles) +} + +func (module *setter) activatePendingUser(ctx context.Context, user *types.User, opts ...root.CreateUserOption) error { + createUserOpts := root.NewCreateUserOptions(opts...) + + if len(createUserOpts.RoleNames) > 0 { + err := module.authz.Grant( + ctx, + user.OrgID, + createUserOpts.RoleNames, + authtypes.MustNewSubject(authtypes.TypeableUser, user.ID.StringValue(), user.OrgID, nil), + ) + if err != nil { + return err + } + } + if err := user.UpdateStatus(types.UserStatusActive); err != nil { return err } - err = module.store.UpdateUser(ctx, user.OrgID, user) + + err := module.store.RunInTx(ctx, func(ctx context.Context) error { + if err := module.store.UpdateUser(ctx, user.OrgID, user); err != nil { + return err + } + + if len(createUserOpts.RoleNames) > 0 { + // delete old user_role entries and create new ones from SSO + if err := module.userRoleStore.DeleteUserRoles(ctx, user.ID); err != nil { + return err + } + + return module.createUserRoleEntries(ctx, user.OrgID, user.ID, createUserOpts.RoleNames) + } + + return nil + }) if err != nil { return err } + traitsOrProperties := types.NewTraitsFromUser(user) + module.analytics.TrackUser(ctx, user.OrgID.String(), user.ID.String(), "User Activated", traitsOrProperties) + return nil } + +func (module *setter) UpdateUserRoles(ctx context.Context, orgID, userID valuer.UUID, finalRoleNames []string) error { + return module.store.RunInTx(ctx, func(ctx context.Context) error { + // delete old user_role entries and create new ones from SSO + if err := module.userRoleStore.DeleteUserRoles(ctx, userID); err != nil { + return err + } + + // create fresh ones + return module.createUserRoleEntries(ctx, orgID, userID, finalRoleNames) + }) +} + +func roleNamesFromUserRoles(userRoles []*authtypes.UserRole) []string { + names := make([]string, 0, len(userRoles)) + for _, ur := range userRoles { + if ur.Role != nil { + names = append(names, ur.Role.Name) + } + } + return names +} diff --git a/pkg/modules/user/impluser/store.go b/pkg/modules/user/impluser/store.go index 6dd242dae73..45619f2666c 100644 --- a/pkg/modules/user/impluser/store.go +++ b/pkg/modules/user/impluser/store.go @@ -52,23 +52,6 @@ func (store *store) CreateUser(ctx context.Context, user *types.User) error { return nil } -func (store *store) GetUsersByEmail(ctx context.Context, email valuer.Email) ([]*types.User, error) { - var users []*types.User - - err := store. - sqlstore. - BunDBCtx(ctx). - NewSelect(). - Model(&users). - Where("email = ?", email). - Scan(ctx) - if err != nil { - return nil, err - } - - return users, nil -} - func (store *store) GetUser(ctx context.Context, id valuer.UUID) (*types.User, error) { user := new(types.User) @@ -104,7 +87,7 @@ func (store *store) GetByOrgIDAndID(ctx context.Context, orgID valuer.UUID, id v return user, nil } -func (store *store) GetUsersByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) ([]*types.User, error) { +func (store *store) GetNonDeletedUsersByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) ([]*types.User, error) { var users []*types.User err := store. @@ -114,25 +97,7 @@ func (store *store) GetUsersByEmailAndOrgID(ctx context.Context, email valuer.Em Model(&users). Where("org_id = ?", orgID). Where("email = ?", email). - Scan(ctx) - if err != nil { - return nil, err - } - - return users, nil -} - -func (store *store) GetActiveUsersByRoleAndOrgID(ctx context.Context, role types.Role, orgID valuer.UUID) ([]*types.User, error) { - var users []*types.User - - err := store. - sqlstore. - BunDBCtx(ctx). - NewSelect(). - Model(&users). - Where("org_id = ?", orgID). - Where("role = ?", role). - Where("status = ?", types.UserStatusActive.StringValue()). + Where("status != ?", types.UserStatusDeleted). Scan(ctx) if err != nil { return nil, err @@ -149,7 +114,6 @@ func (store *store) UpdateUser(ctx context.Context, orgID valuer.UUID, user *typ Model(user). Column("display_name"). Column("email"). - Column("role"). Column("is_root"). Column("updated_at"). Column("status"). @@ -162,7 +126,7 @@ func (store *store) UpdateUser(ctx context.Context, orgID valuer.UUID, user *typ return nil } -func (store *store) ListUsersByOrgID(ctx context.Context, orgID valuer.UUID) ([]*types.GettableUser, error) { +func (store *store) ListUsersByOrgID(ctx context.Context, orgID valuer.UUID) ([]*types.User, error) { users := []*types.User{} err := store. diff --git a/pkg/modules/user/impluser/user_role_store.go b/pkg/modules/user/impluser/user_role_store.go new file mode 100644 index 00000000000..6bcf29ea9b0 --- /dev/null +++ b/pkg/modules/user/impluser/user_role_store.go @@ -0,0 +1,83 @@ +package impluser + +import ( + "context" + + "github.com/SigNoz/signoz/pkg/factory" + "github.com/SigNoz/signoz/pkg/sqlstore" + "github.com/SigNoz/signoz/pkg/types/authtypes" + "github.com/SigNoz/signoz/pkg/valuer" + "github.com/uptrace/bun" +) + +type userRoleStore struct { + sqlstore sqlstore.SQLStore + settings factory.ProviderSettings +} + +func NewUserRoleStore(sqlstore sqlstore.SQLStore, settings factory.ProviderSettings) authtypes.UserRoleStore { + return &userRoleStore{sqlstore: sqlstore, settings: settings} +} + +func (store *userRoleStore) ListUserRolesByOrgIDAndUserIDs(ctx context.Context, orgID valuer.UUID, userIDs []valuer.UUID) ([]*authtypes.UserRole, error) { + userRoles := make([]*authtypes.UserRole, 0) + + err := store.sqlstore. + BunDBCtx(ctx). + NewSelect(). + Model(&userRoles). + Join("JOIN users"). + JoinOn("users.id = user_role.user_id"). + Where("users.org_id = ?", orgID). + Where("users.id IN (?)", bun.In(userIDs)). + Relation("Role"). + Scan(ctx) + if err != nil { + return nil, err + } + + return userRoles, nil +} + +func (store *userRoleStore) CreateUserRoles(ctx context.Context, userRoles []*authtypes.UserRole) error { + _, err := store.sqlstore. + BunDBCtx(ctx). + NewInsert(). + Model(&userRoles). + Exec(ctx) + if err != nil { + return store.sqlstore.WrapAlreadyExistsErrf(err, authtypes.ErrCodeUserRoleAlreadyExists, "duplicate role assignments for user") + } + return nil +} + +func (store *userRoleStore) DeleteUserRoles(ctx context.Context, userID valuer.UUID) error { + _, err := store.sqlstore. + BunDBCtx(ctx). + NewDelete(). + Model(new(authtypes.UserRole)). + Where("user_id = ?", userID). + Exec(ctx) + if err != nil { + return err + } + + return nil +} + +func (store *userRoleStore) GetUserRolesByUserID(ctx context.Context, userID valuer.UUID) ([]*authtypes.UserRole, error) { + userRoles := make([]*authtypes.UserRole, 0) + + err := store.sqlstore. + BunDBCtx(ctx). + NewSelect(). + Model(&userRoles). + Where("user_id = ?", userID). + Relation("Role"). + Scan(ctx) + if err != nil { + return nil, err + } + + return userRoles, nil +} diff --git a/pkg/modules/user/option.go b/pkg/modules/user/option.go index 3d17146fb92..e6ddc3abf3c 100644 --- a/pkg/modules/user/option.go +++ b/pkg/modules/user/option.go @@ -7,6 +7,7 @@ import ( type createUserOptions struct { FactorPassword *types.FactorPassword + RoleNames []string } type CreateUserOption func(*createUserOptions) @@ -17,9 +18,16 @@ func WithFactorPassword(factorPassword *types.FactorPassword) CreateUserOption { } } +func WithRoleNames(roleNames []string) CreateUserOption { + return func(o *createUserOptions) { + o.RoleNames = roleNames + } +} + func NewCreateUserOptions(opts ...CreateUserOption) *createUserOptions { o := &createUserOptions{ FactorPassword: nil, + RoleNames: nil, } for _, opt := range opts { diff --git a/pkg/modules/user/user.go b/pkg/modules/user/user.go index cd26bef56a6..3883726f3aa 100644 --- a/pkg/modules/user/user.go +++ b/pkg/modules/user/user.go @@ -6,10 +6,11 @@ import ( "github.com/SigNoz/signoz/pkg/statsreporter" "github.com/SigNoz/signoz/pkg/types" + "github.com/SigNoz/signoz/pkg/types/authtypes" "github.com/SigNoz/signoz/pkg/valuer" ) -type Module interface { +type Setter interface { // Creates the organization and the first user of that organization. CreateFirstUser(ctx context.Context, organization *types.Organization, name string, email valuer.Email, password string) (*types.User, error) @@ -33,10 +34,10 @@ type Module interface { // Initiate forgot password flow for a user ForgotPassword(ctx context.Context, orgID valuer.UUID, email valuer.Email, frontendBaseURL string) error - UpdateUser(ctx context.Context, orgID valuer.UUID, id string, user *types.User, updatedBy string) (*types.User, error) + UpdateUser(ctx context.Context, orgID valuer.UUID, id string, user *types.DeprecatedUser, updatedBy string) (*types.DeprecatedUser, error) // UpdateAnyUser updates a user and persists the changes to the database along with the analytics and identity deletion. - UpdateAnyUser(ctx context.Context, orgID valuer.UUID, user *types.User) error + UpdateAnyUser(ctx context.Context, orgID valuer.UUID, user *types.DeprecatedUser) error DeleteUser(ctx context.Context, orgID valuer.UUID, id string, deletedBy string) error // invite @@ -49,26 +50,24 @@ type Module interface { RevokeAPIKey(ctx context.Context, id, removedByUserID valuer.UUID) error GetAPIKey(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*types.StorableAPIKeyUser, error) - GetNonDeletedUserByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) (*types.User, error) + // Roles + UpdateUserRoles(ctx context.Context, orgID, userID valuer.UUID, finalRoleNames []string) error statsreporter.StatsCollector } type Getter interface { // Get root user by org id. - GetRootUserByOrgID(context.Context, valuer.UUID) (*types.User, error) + GetRootUserByOrgID(context.Context, valuer.UUID) (*types.User, []*authtypes.UserRole, error) // Get gets the users based on the given id - ListByOrgID(context.Context, valuer.UUID) ([]*types.User, error) - - // Get users by email. - GetUsersByEmail(context.Context, valuer.Email) ([]*types.User, error) + ListByOrgID(context.Context, valuer.UUID) ([]*types.DeprecatedUser, error) - // Get user by orgID and id. - GetByOrgIDAndID(context.Context, valuer.UUID, valuer.UUID) (*types.User, error) + // Get deprecated user object by orgID and id. + GetDeprecatedUserByOrgIDAndID(context.Context, valuer.UUID, valuer.UUID) (*types.DeprecatedUser, error) // Get user by id. - Get(context.Context, valuer.UUID) (*types.User, error) + Get(context.Context, valuer.UUID) (*types.DeprecatedUser, error) // List users by email and org ids. ListUsersByEmailAndOrgIDs(context.Context, valuer.Email, []valuer.UUID) ([]*types.User, error) @@ -81,6 +80,12 @@ type Getter interface { // Get factor password by user id. GetFactorPasswordByUserID(context.Context, valuer.UUID) (*types.FactorPassword, error) + + // Gets single Non-Deleted user by email and org id + GetNonDeletedUserByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) (*types.User, error) + + // Gets user_role with roles entries from db + GetUserRoles(ctx context.Context, userID valuer.UUID) ([]*authtypes.UserRole, error) } type Handler interface { diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 7ebc0e15b09..180da519998 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -2042,7 +2042,7 @@ func (aH *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) { } organization := types.NewOrganization(req.OrgDisplayName, req.OrgName) - user, errv2 := aH.Signoz.Modules.User.CreateFirstUser(r.Context(), organization, req.Name, req.Email, req.Password) + user, errv2 := aH.Signoz.Modules.UserSetter.CreateFirstUser(r.Context(), organization, req.Name, req.Email, req.Password) if errv2 != nil { render.Error(w, errv2) return diff --git a/pkg/signoz/handler_test.go b/pkg/signoz/handler_test.go index ccc654280ee..31d708372ed 100644 --- a/pkg/signoz/handler_test.go +++ b/pkg/signoz/handler_test.go @@ -48,9 +48,11 @@ func TestNewHandlers(t *testing.T) { flagger, err := flagger.New(context.Background(), instrumentationtest.New().ToProviderSettings(), flagger.Config{}, flagger.MustNewRegistry()) require.NoError(t, err) - userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings), flagger) + userRoleStore := impluser.NewUserRoleStore(sqlstore, providerSettings) - modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter) + userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings), userRoleStore, flagger) + + modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, userRoleStore) querierHandler := querier.NewHandler(providerSettings, nil, nil) handlers := NewHandlers(modules, providerSettings, nil, querierHandler, nil, nil, nil, nil, nil, nil, nil) diff --git a/pkg/signoz/module.go b/pkg/signoz/module.go index 53693392b38..7001143b92b 100644 --- a/pkg/signoz/module.go +++ b/pkg/signoz/module.go @@ -54,7 +54,7 @@ type Modules struct { OrgGetter organization.Getter OrgSetter organization.Setter Preference preference.Module - User user.Module + UserSetter user.Setter UserGetter user.Getter SavedView savedview.Module Apdex apdex.Module @@ -89,10 +89,11 @@ func NewModules( config Config, dashboard dashboard.Module, userGetter user.Getter, + userRoleStore authtypes.UserRoleStore, ) Modules { quickfilter := implquickfilter.NewModule(implquickfilter.NewStore(sqlstore)) orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter) - user := impluser.NewModule(impluser.NewStore(sqlstore, providerSettings), tokenizer, emailing, providerSettings, orgSetter, authz, analytics, config.User) + userSetter := impluser.NewSetter(impluser.NewStore(sqlstore, providerSettings), tokenizer, emailing, providerSettings, orgSetter, authz, analytics, config.User, userRoleStore, userGetter) ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings) return Modules{ @@ -102,13 +103,13 @@ func NewModules( SavedView: implsavedview.NewModule(sqlstore), Apdex: implapdex.NewModule(sqlstore), Dashboard: dashboard, - User: user, + UserSetter: userSetter, UserGetter: userGetter, QuickFilter: quickfilter, TraceFunnel: impltracefunnel.NewModule(impltracefunnel.NewStore(sqlstore)), RawDataExport: implrawdataexport.NewModule(querier), AuthDomain: implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs), - Session: implsession.NewModule(providerSettings, authNs, user, userGetter, implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs), tokenizer, orgGetter), + Session: implsession.NewModule(providerSettings, authNs, userSetter, userGetter, implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs), tokenizer, orgGetter), SpanPercentile: implspanpercentile.NewModule(querier, providerSettings), Services: implservices.NewModule(querier, telemetryStore), MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, ruleStore, dashboard, providerSettings, config.MetricsExplorer), diff --git a/pkg/signoz/module_test.go b/pkg/signoz/module_test.go index 89e2107b8ae..8305da8464a 100644 --- a/pkg/signoz/module_test.go +++ b/pkg/signoz/module_test.go @@ -47,9 +47,11 @@ func TestNewModules(t *testing.T) { flagger, err := flagger.New(context.Background(), instrumentationtest.New().ToProviderSettings(), flagger.Config{}, flagger.MustNewRegistry()) require.NoError(t, err) - userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings), flagger) + userRoleStore := impluser.NewUserRoleStore(sqlstore, providerSettings) - modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter) + userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings), userRoleStore, flagger) + + modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, userRoleStore) reflectVal := reflect.ValueOf(modules) for i := 0; i < reflectVal.NumField(); i++ { diff --git a/pkg/signoz/provider.go b/pkg/signoz/provider.go index 542acbc856d..b2a8acbf640 100644 --- a/pkg/signoz/provider.go +++ b/pkg/signoz/provider.go @@ -188,6 +188,8 @@ func NewSQLMigrationProviderFactories( sqlmigration.NewDeprecateUserInviteFactory(sqlstore, sqlschema), sqlmigration.NewUpdateCloudIntegrationUniqueIndexFactory(sqlstore, sqlschema), sqlmigration.NewUpdatePlannedMaintenanceRuleFactory(sqlstore, sqlschema), + sqlmigration.NewAddUserRoleFactory(sqlstore, sqlschema), + sqlmigration.NewDropUserRoleColumnFactory(sqlstore, sqlschema), ) } @@ -259,7 +261,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au orgGetter, authz, implorganization.NewHandler(modules.OrgGetter, modules.OrgSetter), - impluser.NewHandler(modules.User, modules.UserGetter), + impluser.NewHandler(modules.UserSetter, modules.UserGetter), implsession.NewHandler(modules.Session), implauthdomain.NewHandler(modules.AuthDomain), implpreference.NewHandler(modules.Preference), diff --git a/pkg/signoz/provider_test.go b/pkg/signoz/provider_test.go index 12b88f0f99d..9001d76e04b 100644 --- a/pkg/signoz/provider_test.go +++ b/pkg/signoz/provider_test.go @@ -22,6 +22,7 @@ import ( "github.com/SigNoz/signoz/pkg/tokenizer/tokenizertest" "github.com/SigNoz/signoz/pkg/version" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // This is a test to ensure that provider factories can be created without panicking since @@ -77,12 +78,13 @@ func TestNewProviderFactories(t *testing.T) { }) assert.NotPanics(t, func() { - flagger, err := flagger.New(context.Background(), instrumentationtest.New().ToProviderSettings(), flagger.Config{}, flagger.MustNewRegistry()) - if err != nil { - panic(err) - } + providerSettings := instrumentationtest.New().ToProviderSettings() + ss := sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherEqual) + userRoleStore := impluser.NewUserRoleStore(ss, providerSettings) + flagger, err := flagger.New(context.Background(), providerSettings, flagger.Config{}, flagger.MustNewRegistry()) + require.NoError(t, err) - userGetter := impluser.NewGetter(impluser.NewStore(sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherEqual), instrumentationtest.New().ToProviderSettings()), flagger) + userGetter := impluser.NewGetter(impluser.NewStore(sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherEqual), instrumentationtest.New().ToProviderSettings()), userRoleStore, flagger) orgGetter := implorganization.NewGetter(implorganization.NewStore(sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherEqual)), nil) telemetryStore := telemetrystoretest.New(telemetrystore.Config{Provider: "clickhouse"}, sqlmock.QueryMatcherEqual) NewStatsReporterProviderFactories(telemetryStore, []statsreporter.StatsCollector{}, orgGetter, userGetter, tokenizertest.NewMockTokenizer(t), version.Build{}, analytics.Config{Enabled: true}) diff --git a/pkg/signoz/signoz.go b/pkg/signoz/signoz.go index 0b18814d50d..642b7d0d8aa 100644 --- a/pkg/signoz/signoz.go +++ b/pkg/signoz/signoz.go @@ -293,8 +293,11 @@ func New( return nil, err } - // Initialize user getter - userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings), flagger) + // Initialize user store + userStore := impluser.NewStore(sqlstore, providerSettings) + + // Initialize user role store + userRoleStore := impluser.NewUserRoleStore(sqlstore, providerSettings) licensingProviderFactory := licenseProviderFactory(sqlstore, zeus, orgGetter, analytics) licensing, err := licensingProviderFactory.New( @@ -319,6 +322,9 @@ func New( return nil, err } + // Initialize user getter + userGetter := impluser.NewGetter(userStore, userRoleStore, flagger) + // Initialize notification manager from the available notification manager provider factories nfManager, err := factory.NewProviderFromNamedMap( ctx, @@ -402,7 +408,7 @@ func New( } // Initialize all modules - modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard, userGetter) + modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard, userGetter, userRoleStore) // Initialize identN resolver identNFactories := NewIdentNProviderFactories(sqlstore, tokenizer, orgGetter, userGetter, config.User) @@ -411,7 +417,7 @@ func New( return nil, err } - userService := impluser.NewService(providerSettings, impluser.NewStore(sqlstore, providerSettings), modules.User, orgGetter, authz, config.User.Root) + userService := impluser.NewService(providerSettings, impluser.NewStore(sqlstore, providerSettings), modules.UserGetter, modules.UserSetter, orgGetter, authz, config.User.Root) // Initialize the querier handler via callback (allows EE to decorate with anomaly detection) querierHandler := querierHandlerCallback(providerSettings, querier, analytics) @@ -437,7 +443,7 @@ func New( ruler, modules.Dashboard, modules.SavedView, - modules.User, + modules.UserSetter, licensing, tokenizer, config, diff --git a/pkg/sqlmigration/071_add_user_role.go b/pkg/sqlmigration/071_add_user_role.go new file mode 100644 index 00000000000..321fc9cb405 --- /dev/null +++ b/pkg/sqlmigration/071_add_user_role.go @@ -0,0 +1,195 @@ +package sqlmigration + +import ( + "context" + "time" + + "github.com/SigNoz/signoz/pkg/factory" + "github.com/SigNoz/signoz/pkg/sqlschema" + "github.com/SigNoz/signoz/pkg/sqlstore" + "github.com/SigNoz/signoz/pkg/types" + "github.com/SigNoz/signoz/pkg/valuer" + "github.com/uptrace/bun" + "github.com/uptrace/bun/migrate" +) + +var ( + userRoleToSigNozManagedRoleMap = map[string]string{ + "ADMIN": "signoz-admin", + "EDITOR": "signoz-editor", + "VIEWER": "signoz-viewer", + } +) + +type userRow struct { + ID string `bun:"id"` + Role string `bun:"role"` + OrgID string `bun:"org_id"` +} + +type roleRow struct { + ID string `bun:"id"` + Name string `bun:"name"` + OrgID string `bun:"org_id"` +} + +type orgRoleKey struct { + OrgID string + RoleName string +} + +type addUserRole struct { + sqlstore sqlstore.SQLStore + sqlschema sqlschema.SQLSchema +} + +type userRoleRow struct { + bun.BaseModel `bun:"table:user_role"` + + types.Identifiable + UserID string `bun:"user_id"` + RoleID string `bun:"role_id"` + types.TimeAuditable +} + +func NewAddUserRoleFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] { + return factory.NewProviderFactory(factory.MustNewName("add_user_role"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) { + return &addUserRole{ + sqlstore: sqlstore, + sqlschema: sqlschema, + }, nil + }) +} + +func (migration *addUserRole) Register(migrations *migrate.Migrations) error { + if err := migrations.Register(migration.Up, migration.Down); err != nil { + return err + } + + return nil +} + +func (migration *addUserRole) Up(ctx context.Context, db *bun.DB) error { + tx, err := db.BeginTx(ctx, nil) + if err != nil { + return err + } + + defer func() { + _ = tx.Rollback() + }() + + sqls := [][]byte{} + + tableSQLs := migration.sqlschema.Operator().CreateTable(&sqlschema.Table{ + Name: "user_role", + Columns: []*sqlschema.Column{ + {Name: "id", DataType: sqlschema.DataTypeText, Nullable: false}, + {Name: "user_id", DataType: sqlschema.DataTypeText, Nullable: false}, + {Name: "role_id", DataType: sqlschema.DataTypeText, Nullable: false}, + {Name: "created_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false}, + {Name: "updated_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false}, + }, + PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{ + ColumnNames: []sqlschema.ColumnName{"id"}, + }, + ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{ + { + ReferencingColumnName: sqlschema.ColumnName("user_id"), + ReferencedTableName: sqlschema.TableName("users"), + ReferencedColumnName: sqlschema.ColumnName("id"), + }, + { + ReferencingColumnName: sqlschema.ColumnName("role_id"), + ReferencedTableName: sqlschema.TableName("role"), + ReferencedColumnName: sqlschema.ColumnName("id"), + }, + }, + }) + + sqls = append(sqls, tableSQLs...) + + indexSQLs := migration.sqlschema.Operator().CreateIndex( + &sqlschema.UniqueIndex{ + TableName: "user_role", + ColumnNames: []sqlschema.ColumnName{"user_id", "role_id"}, + }, + ) + + sqls = append(sqls, indexSQLs...) + + for _, sql := range sqls { + if _, err := tx.ExecContext(ctx, string(sql)); err != nil { + return err + } + } + + // fill the new user_role table for existing users + var users []userRow + err = tx.NewSelect().TableExpr("users").ColumnExpr("id, role, org_id").Scan(ctx, &users) + if err != nil { + return err + } + + if len(users) == 0 { + return tx.Commit() + } + + orgIDs := make(map[string]struct{}) + for _, u := range users { + orgIDs[u.OrgID] = struct{}{} + } + + orgIDList := make([]string, 0, len(orgIDs)) + for oid := range orgIDs { + orgIDList = append(orgIDList, oid) + } + + var roles []roleRow + err = tx.NewSelect().TableExpr("role").ColumnExpr("id, name, org_id").Where("org_id IN (?)", bun.In(orgIDList)).Scan(ctx, &roles) + if err != nil { + return err + } + + roleMap := make(map[orgRoleKey]string) + for _, r := range roles { + roleMap[orgRoleKey{OrgID: r.OrgID, RoleName: r.Name}] = r.ID + } + + now := time.Now() + userRoles := make([]*userRoleRow, 0, len(users)) + for _, u := range users { + managedRoleName, ok := userRoleToSigNozManagedRoleMap[u.Role] + if !ok { + managedRoleName = "signoz-viewer" // fallback + } + + roleID := roleMap[orgRoleKey{OrgID: u.OrgID, RoleName: managedRoleName}] + + userRoles = append(userRoles, &userRoleRow{ + Identifiable: types.Identifiable{ID: valuer.GenerateUUID()}, + UserID: u.ID, + RoleID: roleID, + TimeAuditable: types.TimeAuditable{ + CreatedAt: now, + UpdatedAt: now, + }, + }) + } + + if len(userRoles) > 0 { + if _, err := tx.NewInsert().Model(&userRoles).Exec(ctx); err != nil { + return err + } + } + + if err := tx.Commit(); err != nil { + return err + } + + return nil +} + +func (migration *addUserRole) Down(ctx context.Context, db *bun.DB) error { + return nil +} diff --git a/pkg/sqlmigration/072_drop_user_role_column.go b/pkg/sqlmigration/072_drop_user_role_column.go new file mode 100644 index 00000000000..1fb62514a01 --- /dev/null +++ b/pkg/sqlmigration/072_drop_user_role_column.go @@ -0,0 +1,73 @@ +package sqlmigration + +import ( + "context" + + "github.com/SigNoz/signoz/pkg/factory" + "github.com/SigNoz/signoz/pkg/sqlschema" + "github.com/SigNoz/signoz/pkg/sqlstore" + "github.com/uptrace/bun" + "github.com/uptrace/bun/migrate" +) + +type dropUserRoleColumn struct { + sqlStore sqlstore.SQLStore + sqlSchema sqlschema.SQLSchema +} + +func NewDropUserRoleColumnFactory(sqlStore sqlstore.SQLStore, sqlSchema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] { + return factory.NewProviderFactory(factory.MustNewName("drop_user_role_column"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) { + return &dropUserRoleColumn{ + sqlStore: sqlStore, + sqlSchema: sqlSchema, + }, nil + }) +} + +func (migration *dropUserRoleColumn) Register(migrations *migrate.Migrations) error { + if err := migrations.Register(migration.Up, migration.Down); err != nil { + return err + } + + return nil +} + +func (migration *dropUserRoleColumn) Up(ctx context.Context, db *bun.DB) error { + tx, err := db.BeginTx(ctx, nil) + if err != nil { + return err + } + + defer func() { + _ = tx.Rollback() + }() + + table, _, err := migration.sqlSchema.GetTable(ctx, sqlschema.TableName("users")) + if err != nil { + return err + } + + roleColumn := &sqlschema.Column{ + Name: sqlschema.ColumnName("role"), + DataType: sqlschema.DataTypeText, + Nullable: false, + } + + sqls := migration.sqlSchema.Operator().DropColumn(table, roleColumn) + + for _, sql := range sqls { + if _, err := tx.ExecContext(ctx, string(sql)); err != nil { + return err + } + } + + if err := tx.Commit(); err != nil { + return err + } + + return nil +} + +func (migration *dropUserRoleColumn) Down(ctx context.Context, db *bun.DB) error { + return nil +} diff --git a/pkg/statsreporter/analyticsstatsreporter/provider.go b/pkg/statsreporter/analyticsstatsreporter/provider.go index 647765a4c42..14f960b92ae 100644 --- a/pkg/statsreporter/analyticsstatsreporter/provider.go +++ b/pkg/statsreporter/analyticsstatsreporter/provider.go @@ -178,7 +178,7 @@ func (provider *provider) Report(ctx context.Context) error { } for _, user := range users { - traits := types.NewTraitsFromUser(user) + traits := types.NewTraitsFromDeprecatedUser(user) if maxLastObservedAt, ok := maxLastObservedAtPerUserID[user.ID]; ok { traits["auth_token.last_observed_at.max.time"] = maxLastObservedAt.UTC() traits["auth_token.last_observed_at.max.time_unix"] = maxLastObservedAt.Unix() diff --git a/pkg/tokenizer/tokenizerstore/sqltokenizerstore/store.go b/pkg/tokenizer/tokenizerstore/sqltokenizerstore/store.go index 802f45ada13..2880ef3df61 100644 --- a/pkg/tokenizer/tokenizerstore/sqltokenizerstore/store.go +++ b/pkg/tokenizer/tokenizerstore/sqltokenizerstore/store.go @@ -3,6 +3,7 @@ package sqltokenizerstore import ( "context" + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types/authtypes" @@ -35,7 +36,6 @@ func (store *store) Create(ctx context.Context, token *authtypes.StorableToken) func (store *store) GetIdentityByUserID(ctx context.Context, userID valuer.UUID) (*authtypes.Identity, error) { user := new(types.User) - err := store. sqlstore. BunDBCtx(ctx). @@ -47,7 +47,25 @@ func (store *store) GetIdentityByUserID(ctx context.Context, userID valuer.UUID) return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrCodeUserNotFound, "user with id: %s does not exist", userID) } - return authtypes.NewIdentity(userID, user.OrgID, user.Email, types.Role(user.Role), authtypes.IdentNProviderTokenizer), nil + userRoles := make([]*authtypes.UserRole, 0) + err = store.sqlstore. + BunDBCtx(ctx). + NewSelect(). + Model(&userRoles). + Where("user_id = ?", userID). + Relation("Role"). + Scan(ctx) + if err != nil { + return nil, err + } + + if len(userRoles) == 0 { + return nil, errors.Newf(errors.TypeInternal, errors.CodeInternal, "no roles found for user with id: %s", userID) + } + + role := authtypes.SigNozManagedRoleToExistingLegacyRole[userRoles[0].Role.Name] + + return authtypes.NewIdentity(userID, user.OrgID, user.Email, role, authtypes.IdentNProviderTokenizer), nil } func (store *store) GetByAccessToken(ctx context.Context, accessToken string) (*authtypes.StorableToken, error) { diff --git a/pkg/types/authtypes/authn.go b/pkg/types/authtypes/authn.go index da3e9576a21..ce3271f4e3e 100644 --- a/pkg/types/authtypes/authn.go +++ b/pkg/types/authtypes/authn.go @@ -128,7 +128,7 @@ func (typ *Identity) ToClaims() Claims { type AuthNStore interface { // Get user and factor password by email and orgID. - GetActiveUserAndFactorPasswordByEmailAndOrgID(ctx context.Context, email string, orgID valuer.UUID) (*types.User, *types.FactorPassword, error) + GetActiveUserAndFactorPasswordByEmailAndOrgID(ctx context.Context, email string, orgID valuer.UUID) (*types.User, *types.FactorPassword, []*UserRole, error) // Get org domain from id. GetAuthDomainFromID(ctx context.Context, domainID valuer.UUID) (*AuthDomain, error) diff --git a/pkg/types/authtypes/role.go b/pkg/types/authtypes/role.go index 76bd6dfb0fa..b1c2b719a8d 100644 --- a/pkg/types/authtypes/role.go +++ b/pkg/types/authtypes/role.go @@ -48,6 +48,12 @@ var ( types.RoleEditor: SigNozEditorRoleName, types.RoleViewer: SigNozViewerRoleName, } + + SigNozManagedRoleToExistingLegacyRole = map[string]types.Role{ + SigNozAdminRoleName: types.RoleAdmin, + SigNozEditorRoleName: types.RoleEditor, + SigNozViewerRoleName: types.RoleViewer, + } ) var ( diff --git a/pkg/types/authtypes/user_role.go b/pkg/types/authtypes/user_role.go new file mode 100644 index 00000000000..10b41e4dcaf --- /dev/null +++ b/pkg/types/authtypes/user_role.go @@ -0,0 +1,62 @@ +package authtypes + +import ( + "context" + "time" + + "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/valuer" + "github.com/uptrace/bun" +) + +var ( + ErrCodeUserRoleAlreadyExists = errors.MustNewCode("user_role_already_exists") + ErrCodeUserRolesNotFound = errors.MustNewCode("user_roles_not_found") +) + +type UserRole struct { + bun.BaseModel `bun:"table:user_role,alias:user_role"` + + ID valuer.UUID `bun:"id,pk,type:text" json:"id" required:"true"` + UserID valuer.UUID `bun:"user_id" json:"user_id"` + RoleID valuer.UUID `bun:"role_id" json:"role_id"` + CreatedAt time.Time `bun:"created_at" json:"createdAt"` + UpdatedAt time.Time `bun:"updated_at" json:"updatedAt"` + + // read only fields + Role *StorableRole `bun:"rel:belongs-to,join:role_id=id" json:"role"` +} + +func newUserRole(userID valuer.UUID, roleID valuer.UUID) *UserRole { + return &UserRole{ + ID: valuer.GenerateUUID(), + UserID: userID, + RoleID: roleID, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } +} + +func NewUserRoles(userID valuer.UUID, roles []*Role) []*UserRole { + userRoles := make([]*UserRole, len(roles)) + + for idx, role := range roles { + userRoles[idx] = newUserRole(userID, role.ID) + } + + return userRoles +} + +type UserRoleStore interface { + // create user roles in bulk + CreateUserRoles(ctx context.Context, userRoles []*UserRole) error + + // get user roles by user id + GetUserRolesByUserID(ctx context.Context, userID valuer.UUID) ([]*UserRole, error) + + // list all user_role entries for + ListUserRolesByOrgIDAndUserIDs(ctx context.Context, orgID valuer.UUID, userIDs []valuer.UUID) ([]*UserRole, error) + + // delete user role entries by user id + DeleteUserRoles(ctx context.Context, userID valuer.UUID) error +} diff --git a/pkg/types/user.go b/pkg/types/user.go index 4f8d53b66e2..532270bf372 100644 --- a/pkg/types/user.go +++ b/pkg/types/user.go @@ -33,15 +33,12 @@ var ( ValidUserStatus = []valuer.String{UserStatusPendingInvite, UserStatusActive, UserStatusDeleted} ) -type GettableUser = User - type User struct { - bun.BaseModel `bun:"table:users"` + bun.BaseModel `bun:"table:users,alias:users"` Identifiable DisplayName string `bun:"display_name" json:"displayName"` Email valuer.Email `bun:"email" json:"email"` - Role Role `bun:"role" json:"role"` OrgID valuer.UUID `bun:"org_id" json:"orgId"` IsRoot bool `bun:"is_root" json:"isRoot"` Status valuer.String `bun:"status" json:"status"` @@ -49,6 +46,11 @@ type User struct { TimeAuditable } +type DeprecatedUser struct { + *User + Role Role `json:"role"` +} + type PostableRegisterOrgAndAdmin struct { Name string `json:"name"` Email valuer.Email `json:"email"` @@ -57,15 +59,11 @@ type PostableRegisterOrgAndAdmin struct { OrgName string `json:"orgName"` } -func NewUser(displayName string, email valuer.Email, role Role, orgID valuer.UUID, status valuer.String) (*User, error) { +func NewUser(displayName string, email valuer.Email, orgID valuer.UUID, status valuer.String) (*User, error) { if email.IsZero() { return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "email is required") } - if role == "" { - return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "role is required") - } - if orgID.IsZero() { return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgID is required") } @@ -80,7 +78,6 @@ func NewUser(displayName string, email valuer.Email, role Role, orgID valuer.UUI }, DisplayName: displayName, Email: email, - Role: role, OrgID: orgID, IsRoot: false, Status: status, @@ -106,7 +103,6 @@ func NewRootUser(displayName string, email valuer.Email, orgID valuer.UUID) (*Us }, DisplayName: displayName, Email: email, - Role: RoleAdmin, OrgID: orgID, IsRoot: true, Status: UserStatusActive, @@ -117,9 +113,36 @@ func NewRootUser(displayName string, email valuer.Email, orgID valuer.UUID) (*Us }, nil } +func NewDeprecatedUserFromUserAndRole(user *User, role Role) *DeprecatedUser { + return &DeprecatedUser{ + user, + role, + } +} + +func NewUserFromDeprecatedUser(deprecatedUser *DeprecatedUser) *User { + return &User{ + Identifiable: deprecatedUser.Identifiable, + DisplayName: deprecatedUser.DisplayName, + Email: deprecatedUser.Email, + OrgID: deprecatedUser.OrgID, + IsRoot: deprecatedUser.IsRoot, + Status: deprecatedUser.Status, + DeletedAt: deprecatedUser.DeletedAt, + TimeAuditable: deprecatedUser.TimeAuditable, + } +} + // Update applies mutable fields from the input to the user. Immutable fields // (email, is_root, org_id, id) are preserved. Only non-zero input fields are applied. -func (u *User) Update(displayName string, role Role) { +func (u *User) Update(displayName string) { + if displayName != "" { + u.DisplayName = displayName + } + u.UpdatedAt = time.Now() +} + +func (u *DeprecatedUser) Update(displayName string, role Role) { if displayName != "" { u.DisplayName = displayName } @@ -149,7 +172,6 @@ func (u *User) UpdateStatus(status valuer.String) error { // PromoteToRoot promotes the user to a root user with admin role. func (u *User) PromoteToRoot() { u.IsRoot = true - u.Role = RoleAdmin u.UpdatedAt = time.Now() } @@ -187,6 +209,16 @@ func (u *User) ErrIfPending() error { } func NewTraitsFromUser(user *User) map[string]any { + return map[string]any{ + "name": user.DisplayName, + "email": user.Email.String(), + "display_name": user.DisplayName, + "status": user.Status, + "created_at": user.CreatedAt, + } +} + +func NewTraitsFromDeprecatedUser(user *DeprecatedUser) map[string]any { return map[string]any{ "name": user.DisplayName, "role": user.Role, @@ -224,13 +256,7 @@ type UserStore interface { GetByOrgIDAndID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*User, error) // Get user by email and orgID. - GetUsersByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) ([]*User, error) - - // Get users by email. - GetUsersByEmail(ctx context.Context, email valuer.Email) ([]*User, error) - - // Get users by role and org. - GetActiveUsersByRoleAndOrgID(ctx context.Context, role Role, orgID valuer.UUID) ([]*User, error) + GetNonDeletedUsersByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) ([]*User, error) // List users by org. ListUsersByOrgID(ctx context.Context, orgID valuer.UUID) ([]*User, error) diff --git a/tests/integration/src/passwordauthn/08_user_unique_index.py b/tests/integration/src/passwordauthn/08_user_unique_index.py index 5c401901511..ae334d6a58a 100644 --- a/tests/integration/src/passwordauthn/08_user_unique_index.py +++ b/tests/integration/src/passwordauthn/08_user_unique_index.py @@ -117,15 +117,14 @@ def test_unique_index_allows_multiple_deleted_rows( conn.execute( sql.text( "INSERT INTO users" - " (id, display_name, email, role, org_id, is_root, status, created_at, updated_at, deleted_at)" - " VALUES (:id, :display_name, :email, :role, :org_id," + " (id, display_name, email, org_id, is_root, status, created_at, updated_at, deleted_at)" + " VALUES (:id, :display_name, :email, :org_id," " false, 'active', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, :zero_time)" ), { "id": active_id, "display_name": "first active row", "email": UNIQUE_INDEX_USER_EMAIL, - "role": "EDITOR", "org_id": org_id, "zero_time": "0001-01-01 00:00:00", }, @@ -137,15 +136,14 @@ def test_unique_index_allows_multiple_deleted_rows( conn.execute( sql.text( "INSERT INTO users" - " (id, display_name, email, role, org_id, is_root, status, created_at, updated_at, deleted_at)" - " VALUES (:id, :display_name, :email, :role, :org_id," + " (id, display_name, email, org_id, is_root, status, created_at, updated_at, deleted_at)" + " VALUES (:id, :display_name, :email, :org_id," " false, 'active', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, :zero_time)" ), { "id": str(uuid.uuid4()), "display_name": "should violate index", "email": UNIQUE_INDEX_USER_EMAIL, - "role": "EDITOR", "org_id": org_id, "zero_time": "0001-01-01 00:00:00", }, @@ -156,15 +154,14 @@ def test_unique_index_allows_multiple_deleted_rows( conn.execute( sql.text( "INSERT INTO users" - " (id, display_name, email, role, org_id, is_root, status, created_at, updated_at, deleted_at)" - " VALUES (:id, :display_name, :email, :role, :org_id," + " (id, display_name, email, org_id, is_root, status, created_at, updated_at, deleted_at)" + " VALUES (:id, :display_name, :email, :org_id," " false, 'deleted', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)" ), { "id": str(uuid.uuid4()), "display_name": "third deleted row", "email": UNIQUE_INDEX_USER_EMAIL, - "role": "EDITOR", "org_id": org_id, }, ) From b1efb66197709fd77d45af7376dafcc071fe9dfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vinicius=20Louren=C3=A7o?= <12551007+H4ad@users.noreply.github.com> Date: Mon, 23 Mar 2026 11:03:35 -0300 Subject: [PATCH 11/78] chore(cursor): add rules & skills to help migration (#10405) --- frontend/.cursor/rules/state-management.mdc | 97 +++++++++++ .../skills/migrate-state-management/SKILL.md | 150 ++++++++++++++++++ .../migrate-state-management/reference.md | 50 ++++++ 3 files changed, 297 insertions(+) create mode 100644 frontend/.cursor/rules/state-management.mdc create mode 100644 frontend/.cursor/skills/migrate-state-management/SKILL.md create mode 100644 frontend/.cursor/skills/migrate-state-management/reference.md diff --git a/frontend/.cursor/rules/state-management.mdc b/frontend/.cursor/rules/state-management.mdc new file mode 100644 index 00000000000..fb5c02ae1d0 --- /dev/null +++ b/frontend/.cursor/rules/state-management.mdc @@ -0,0 +1,97 @@ +--- +globs: **/*.store.ts +alwaysApply: false +--- +# State Management: React Query, nuqs, Zustand + +Use the following stack. Do **not** introduce or recommend Redux or React Context for shared/global state. + +## Server state → React Query + +- **Use for:** API responses, time-series data, caching, background refetch, retries, stale/refresh. +- **Do not use Redux/Context** to store or mirror data that comes from React Query (e.g. do not dispatch API results into Redux). +- Prefer generated React Query hooks from `frontend/src/api/generated` when available. +- Keep server state in React Query; expose it via hooks that return the query result (and optionally memoized derived values). Do not duplicate it in Redux or Context. + +```tsx +// ✅ GOOD: single source of truth from React Query +export function useAppStateHook() { + const { data, isError } = useQuery(...) + const memoizedConfigs = useMemo(() => ({ ... }), [data?.configs]) + return { configs: memoizedConfigs, isError, ... } +} + +// ❌ BAD: copying React Query result into Redux +dispatch({ type: UPDATE_LATEST_VERSION, payload: queryResponse.data }) +``` + +## URL state → nuqs + +- **Use for:** shareable state, filters, time range, selected values, pagination, view state that belongs in the URL. +- **Do not use Redux/Context** for state that should be shareable or reflected in the URL. +- Use [nuqs](https://nuqs.dev/docs/basic-usage) for typed, type-safe URL search params. Avoid ad-hoc `useSearchParams` encoding/decoding. +- Keep URL payload small; respect browser URL length limits (e.g. Chrome ~2k chars). Do not put large datasets or sensitive data in query params. + +```tsx +// ✅ GOOD: nuqs for filters / time range / selection +const [timeRange, setTimeRange] = useQueryState('timeRange', parseAsString.withDefault('1h')) +const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1)) + +// ❌ BAD: Redux/Context for shareable or URL-synced state +const { timeRange } = useContext(SomeContext) +``` + +## Client state → Zustand + +- **Use for:** global/client state, cross-component state, feature flags, complex or large client objects (e.g. dashboard state, query builder state). +- **Do not use Redux or React Context** for global or feature-level client state. +- Prefer small, domain-scoped stores (e.g. DashboardStore, QueryBuilderStore). + +### Zustand best practices (align with eslint-plugin-zustand-rules) + +- **One store per module.** Do not define multiple `create()` calls in the same file; use one store per module (or compose slices into one store). +- **Always use selectors.** Call the store hook with a selector so only the used slice triggers re-renders. Never use `useStore()` with no selector. + +```tsx +// ✅ GOOD: selector — re-renders only when isDashboardLocked changes +const isLocked = useDashboardStore(state => state.isDashboardLocked) + +// ❌ BAD: no selector — re-renders on any store change +const state = useDashboardStore() +``` + +- **Never mutate state directly.** Update only via `set` or `setState` (or `getState()` + `set` for reads). No `state.foo = x` or `state.bears += 1` inside actions. + +```tsx +// ✅ GOOD: use set +increment: () => set(state => ({ bears: state.bears + 1 })) + +// ❌ BAD: direct mutation +increment: () => { state.bears += 1 } +``` + +- **State properties before actions.** In the store object, list all state fields first, then action functions. +- **Split into slices when state is large.** If a store has many top-level properties (e.g. more than 5–10), split into slice factories and combine with one `create()`. + +```tsx +// ✅ GOOD: slices for large state +const createBearSlice = set => ({ bears: 0, addBear: () => set(s => ({ bears: s.bears + 1 })) }) +const createFishSlice = set => ({ fish: 0, addFish: () => set(s => ({ fish: s.fish + 1 })) }) +const useStore = create(set => ({ ...createBearSlice(set), ...createFishSlice(set) })) +``` + +- **In projects using Zustand:** add `eslint-plugin-zustand-rules` and extend `plugin:zustand-rules/recommended` to enforce these rules automatically. + +## Local state → React state only + +- **Use useState/useReducer** for: component-local UI state, form inputs, toggles, hover state, data that never leaves the component. +- Do not use Zustand, Redux, or Context for state that is purely local to one component or a small subtree. + +## Summary + +| State type | Use | Avoid | +|-------------------|------------------|--------------------| +| Server / API | React Query | Redux, Context | +| URL / shareable | nuqs | Redux, Context | +| Global client | Zustand | Redux, Context | +| Local UI | useState/useReducer | Zustand, Redux, Context | diff --git a/frontend/.cursor/skills/migrate-state-management/SKILL.md b/frontend/.cursor/skills/migrate-state-management/SKILL.md new file mode 100644 index 00000000000..e086689aa0d --- /dev/null +++ b/frontend/.cursor/skills/migrate-state-management/SKILL.md @@ -0,0 +1,150 @@ +--- +name: migrate-state-management +description: Migrate Redux or React Context to the correct state option (React Query for server state, nuqs for URL/shareable state, Zustand for global client state). Use when refactoring away from Redux/Context, moving state to the right store, or when the user asks to migrate state management. +--- + +# Migrate State: Redux/Context → React Query, nuqs, Zustand + +Do **not** introduce or recommend Redux or React Context. Migrate existing usage to the stack below. + +## 1. Classify the state + +Before changing code, classify what the state represents: + +| If the state is… | Migrate to | Do not use | +|------------------|------------|------------| +| From API / server (versions, configs, fetched lists, time-series) | **React Query** | Redux, Context | +| Shareable via URL (filters, time range, page, selected ids) | **nuqs** | Redux, Context | +| Global/client UI (dashboard lock, query builder, feature flags, large client objects) | **Zustand** | Redux, Context | +| Local to one component (inputs, toggles, hover) | **useState / useReducer** | Zustand, Redux, Context | + +If one slice mixes concerns (e.g. Redux has both API data and pagination), split: API → React Query, pagination → nuqs, rest → Zustand or local state. + +## 2. Migrate to React Query (server state) + +**When:** State comes from or mirrors an API response (e.g. `currentVersion`, `latestVersion`, `configs`, lists). + +**Steps:** + +1. Find where the data is fetched (existing `useQuery`/API call) and where it is dispatched or set in Context/Redux. +2. Remove the dispatch/set that writes API results into Redux/Context. +3. Expose a single hook that uses the query and returns the same shape consumers expect (use `useMemo` for derived objects like `configs` to avoid unnecessary re-renders). +4. Replace Redux/Context consumption with the new hook. Prefer generated React Query hooks from `frontend/src/api/generated` when available. +5. Configure cache/refetch (e.g. `refetchOnMount: false`, `staleTime`) so behavior matches previous “single source” expectations. + +**Before (Redux mirroring React Query):** + +```tsx +if (getUserLatestVersionResponse.isFetched && getUserLatestVersionResponse.isSuccess && getUserLatestVersionResponse.data?.payload) { + dispatch({ type: UPDATE_LATEST_VERSION, payload: { latestVersion: getUserLatestVersionResponse.data.payload.tag_name } }) +} +``` + +**After (single source in React Query):** + +```tsx +export function useAppStateHook() { + const { data, isError } = useQuery(...) + const memoizedConfigs = useMemo(() => ({ ... }), [data?.configs]) + return { + latestVersion: data?.payload?.tag_name, + configs: memoizedConfigs, + isError, + } +} +``` + +Consumers use `useAppStateHook()` instead of `useSelector` or Context. Do not copy React Query result into Redux or Context. + +## 3. Migrate to nuqs (URL / shareable state) + +**When:** State should be in the URL: filters, time range, pagination, selected values, view state. Keep payload small (e.g. Chrome ~2k chars); no large datasets or sensitive data. + +**Steps:** + +1. Identify which Redux/Context fields are shareable or already reflected in the URL (e.g. `currentPage`, `timeRange`, `selectedFilter`). +2. Add nuqs (or use existing): `useQueryState('param', parseAsString.withDefault('…'))` (or `parseAsInteger`, etc.). +3. Replace reads/writes of those fields with nuqs hooks. Use typed parsers; avoid ad-hoc `useSearchParams` encoding/decoding. +4. Remove the same fields from Redux/Context and their reducers/providers. + +**Before (Context/Redux):** + +```tsx +const { timeRange } = useContext(SomeContext) +const [page, setPage] = useDispatch(...) +``` + +**After (nuqs):** + +```tsx +const [timeRange, setTimeRange] = useQueryState('timeRange', parseAsString.withDefault('1h')) +const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1)) +``` + +## 4. Migrate to Zustand (global client state) + +**When:** State is global or cross-component client state: feature flags, dashboard state, query builder state, complex/large client objects (e.g. up to ~1.5–2MB). Not for server cache or local-only UI. + +**Steps:** + +1. Create one store per domain (e.g. `DashboardStore`, `QueryBuilderStore`). One `create()` per module; for large state use slice factories and combine. +2. Put state properties first, then actions. Use `set` (or `setState` / `getState()` + `set`) for updates; never mutate state directly. +3. Replace Context/Redux consumption with the store hook **and a selector** so only the used slice triggers re-renders. +4. Remove the old Context provider / Redux slice and related dispatches. + +**Selector (required):** + +```tsx +const isLocked = useDashboardStore(state => state.isDashboardLocked) +``` + +Never use `useStore()` with no selector. Never do `state.foo = x` inside actions; use `set(state => ({ ... }))`. + +**Before (Context/Redux):** + +```tsx +const { isDashboardLocked, setLocked } = useContext(DashboardContext) +``` + +**After (Zustand):** + +```tsx +const isLocked = useDashboardStore(state => state.isDashboardLocked) +const setLocked = useDashboardStore(state => state.setLocked) +``` + +For large stores (many top-level fields), split into slices and combine: + +```tsx +const createBearSlice = set => ({ bears: 0, addBear: () => set(s => ({ bears: s.bears + 1 })) }) +const useStore = create(set => ({ ...createBearSlice(set), ...createFishSlice(set) })) +``` + +Add `eslint-plugin-zustand-rules` with `plugin:zustand-rules/recommended` to enforce selectors and no direct mutation. + +## 5. Migrate to local state (useState / useReducer) + +**When:** State is used only inside one component or a small subtree (form inputs, toggles, hover, panel selection). No URL sync, no cross-feature sharing. + +**Steps:** + +1. Move the state into the component that owns it (or the smallest common parent). +2. Use `useState` or `useReducer` (useReducer when multiple related fields change together). +3. Remove from Redux/Context and any provider/slice. + +Do not use Zustand, Redux, or Context for purely local UI state. + +## 6. Migration checklist + +- [ ] Classify each piece of state (server / URL / global client / local). +- [ ] Server state: move to React Query; expose via hook; remove Redux/Context mirroring. +- [ ] URL state: move to nuqs; remove from Redux/Context; keep URL payload small. +- [ ] Global client state: move to Zustand with selectors and immutable updates; one store per domain. +- [ ] Local state: move to useState/useReducer in the owning component. +- [ ] Remove old Redux slices / Context providers and all dispatches/consumers for migrated state. +- [ ] Do not duplicate the same data in multiple places (e.g. React Query + Redux). + +## Additional resources + +- Project rule: [.cursor/rules/state-management.mdc](../../rules/state-management.mdc) +- Detailed patterns and rationale: [reference.md](reference.md) diff --git a/frontend/.cursor/skills/migrate-state-management/reference.md b/frontend/.cursor/skills/migrate-state-management/reference.md new file mode 100644 index 00000000000..0ca9687f0a2 --- /dev/null +++ b/frontend/.cursor/skills/migrate-state-management/reference.md @@ -0,0 +1,50 @@ +# State migration reference + +## Why migrate + +- **Context:** Re-renders all consumers on any change; no granular subscriptions; becomes brittle at scale. +- **Redux:** Heavy boilerplate (actions, reducers, selectors, Provider); slower onboarding; often used to mirror React Query or URL state. +- **Goal:** Fewer mechanisms, domain isolation, granular subscriptions, single source of truth per state type. + +## React Query migration (server state) + +Typical anti-pattern: API is called via React Query, then result is dispatched to Redux. Flow becomes: Component → useQueries → API → dispatch → Reducer → Redux state → useSelector. + +Correct flow: Component → useQuery (or custom hook wrapping it) → same component reads from hook. No Redux/Context in between. + +- Prefer generated hooks from `frontend/src/api/generated`. +- For “app state” that is just API data (versions, configs), one hook that returns `{ ...data, configs: useMemo(...) }` is enough. No selectors needed for plain data; useMemo only where the value is used as dependency (e.g. in useState). +- Set `staleTime` / `refetchOnMount` etc. so refetch behavior matches previous expectations. + +## nuqs migration (URL state) + +Redux/Context often hold pagination, filters, time range, selected values that are shareable. Those belong in the URL. + +- Use [nuqs](https://nuqs.dev/docs/basic-usage) for typed search params. Avoid ad-hoc `useSearchParams` + manual encoding. +- Browser limits: Chrome ~2k chars practical; keep payload small; no large datasets or secrets in query params. +- If the app uses TanStack Router, search params can be handled there; otherwise nuqs is the standard. + +## Zustand migration (client state) + +- One store per domain (e.g. DashboardStore, QueryBuilderStore). Multiple `create()` in one file is disallowed; use one store or composed slices. +- Always use a selector: `useStore(s => s.field)` so only that field drives re-renders. +- Never mutate: update only via `set(state => ({ ... }))` or `setState` / `getState()` + `set`. +- State properties first, then actions. For 5–10+ top-level fields, split into slice factories and combine with one `create()`. +- Large client objects: Zustand is for “large” in the ~1.5–2MB range; above that, optimize at API/store design. +- Testing: no Provider; stores are plain functions; easy to reset and mock. + +## What not to use + +- **Redux / Context** for new or migrated shared/global state. +- **Redux / Context** to store or mirror React Query results. +- **Redux / Context** for state that should live in the URL (use nuqs). +- **Zustand / Redux / Context** for component-local UI (use useState/useReducer). + +## Summary table + +| State type | Use | Avoid | +|-------------|--------------------|-----------------| +| Server/API | React Query | Redux, Context | +| URL/shareable | nuqs | Redux, Context | +| Global client | Zustand | Redux, Context | +| Local UI | useState/useReducer | Zustand, Redux, Context | From 866e541e29cf41e95440a6c5f58cc8af55dee066 Mon Sep 17 00:00:00 2001 From: swapnil-signoz Date: Mon, 23 Mar 2026 19:55:32 +0530 Subject: [PATCH 12/78] refactor: cloud integration store implementation (#10469) * feat: adding cloud integration type for refactor * refactor: store interfaces to use local types and error * feat: adding sql store implementation * refactor: removing interface check * feat: adding updated types for cloud integration * refactor: using struct for map * refactor: update cloud integration types and module interface * fix: correct GetService signature and remove shadowed Data field * feat: implement cloud integration store * refactor: adding comments and removed wrong code * refactor: streamlining types * refactor: add comments for backward compatibility in PostableAgentCheckInRequest * refactor: update Dashboard struct comments and remove unused fields * refactor: split upsert store method * feat: adding integration test * refactor: clean up types * refactor: renaming service type to service id * refactor: using serviceID type * feat: adding method for service id creation * refactor: updating store methods * refactor: clean up * refactor: clean up * refactor: review comments * refactor: clean up * fix: update error code for service not found * refactor: returning error only for create methods * refactor: method chaining formatting --- .../implcloudintegration/store.go | 174 ++++++++++++++++++ .../cloudintegrationtypes/cloudintegration.go | 5 +- pkg/types/cloudintegrationtypes/store.go | 22 +-- 3 files changed, 189 insertions(+), 12 deletions(-) create mode 100644 pkg/modules/cloudintegration/implcloudintegration/store.go diff --git a/pkg/modules/cloudintegration/implcloudintegration/store.go b/pkg/modules/cloudintegration/implcloudintegration/store.go new file mode 100644 index 00000000000..8e8f4a6513c --- /dev/null +++ b/pkg/modules/cloudintegration/implcloudintegration/store.go @@ -0,0 +1,174 @@ +package implcloudintegration + +import ( + "context" + "time" + + "github.com/SigNoz/signoz/pkg/sqlstore" + "github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes" + "github.com/SigNoz/signoz/pkg/valuer" +) + +type store struct { + store sqlstore.SQLStore +} + +func NewStore(sqlStore sqlstore.SQLStore) cloudintegrationtypes.Store { + return &store{store: sqlStore} +} + +func (store *store) GetAccountByID(ctx context.Context, orgID, id valuer.UUID, provider cloudintegrationtypes.CloudProviderType) (*cloudintegrationtypes.StorableCloudIntegration, error) { + account := new(cloudintegrationtypes.StorableCloudIntegration) + err := store. + store. + BunDBCtx(ctx). + NewSelect(). + Model(account). + Where("id = ?", id). + Where("org_id = ?", orgID). + Where("provider = ?", provider). + Scan(ctx) + if err != nil { + return nil, store.store.WrapNotFoundErrf(err, cloudintegrationtypes.ErrCodeCloudIntegrationNotFound, "cloud integration account with id %s not found", id) + } + return account, nil +} + +func (store *store) ListConnectedAccounts(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) ([]*cloudintegrationtypes.StorableCloudIntegration, error) { + var accounts []*cloudintegrationtypes.StorableCloudIntegration + err := store. + store. + BunDBCtx(ctx). + NewSelect(). + Model(&accounts). + Where("org_id = ?", orgID). + Where("provider = ?", provider). + Where("removed_at IS NULL"). + Where("account_id IS NOT NULL"). + Where("last_agent_report IS NOT NULL"). + Order("created_at ASC"). + Scan(ctx) + if err != nil { + return nil, err + } + return accounts, nil +} + +func (store *store) CreateAccount(ctx context.Context, account *cloudintegrationtypes.StorableCloudIntegration) error { + _, err := store. + store. + BunDBCtx(ctx). + NewInsert(). + Model(account). + Exec(ctx) + if err != nil { + return store.store.WrapAlreadyExistsErrf(err, cloudintegrationtypes.ErrCodeCloudIntegrationAlreadyExists, "cloud integration account with id %s already exists", account.ID) + } + + return nil +} + +func (store *store) UpdateAccount(ctx context.Context, account *cloudintegrationtypes.StorableCloudIntegration) error { + _, err := store. + store. + BunDBCtx(ctx). + NewUpdate(). + Model(account). + WherePK(). + Where("org_id = ?", account.OrgID). + Where("provider = ?", account.Provider). + Exec(ctx) + + return err +} + +func (store *store) RemoveAccount(ctx context.Context, orgID, id valuer.UUID, provider cloudintegrationtypes.CloudProviderType) error { + _, err := store. + store. + BunDBCtx(ctx). + NewUpdate(). + Model(new(cloudintegrationtypes.StorableCloudIntegration)). + Set("removed_at = ?", time.Now()). + Where("id = ?", id). + Where("org_id = ?", orgID). + Where("provider = ?", provider). + Exec(ctx) + return err +} + +func (store *store) GetConnectedAccount(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType, providerAccountID string) (*cloudintegrationtypes.StorableCloudIntegration, error) { + account := new(cloudintegrationtypes.StorableCloudIntegration) + err := store. + store. + BunDBCtx(ctx). + NewSelect(). + Model(account). + Where("org_id = ?", orgID). + Where("provider = ?", provider). + Where("account_id = ?", providerAccountID). + Where("last_agent_report IS NOT NULL"). + Where("removed_at IS NULL"). + Scan(ctx) + if err != nil { + return nil, store.store.WrapNotFoundErrf(err, cloudintegrationtypes.ErrCodeCloudIntegrationNotFound, "connected account with provider account id %s not found", providerAccountID) + } + return account, nil +} + +func (store *store) GetServiceByServiceID(ctx context.Context, cloudIntegrationID valuer.UUID, serviceID cloudintegrationtypes.ServiceID) (*cloudintegrationtypes.StorableCloudIntegrationService, error) { + service := new(cloudintegrationtypes.StorableCloudIntegrationService) + err := store. + store. + BunDBCtx(ctx). + NewSelect(). + Model(service). + Where("cloud_integration_id = ?", cloudIntegrationID). + Where("type = ?", serviceID). + Scan(ctx) + if err != nil { + return nil, store.store.WrapNotFoundErrf(err, cloudintegrationtypes.ErrCodeCloudIntegrationServiceNotFound, "cloud integration service with id %s not found", serviceID) + } + return service, nil +} + +func (store *store) ListServices(ctx context.Context, cloudIntegrationID valuer.UUID) ([]*cloudintegrationtypes.StorableCloudIntegrationService, error) { + var services []*cloudintegrationtypes.StorableCloudIntegrationService + err := store. + store. + BunDBCtx(ctx). + NewSelect(). + Model(&services). + Where("cloud_integration_id = ?", cloudIntegrationID). + Scan(ctx) + if err != nil { + return nil, err + } + return services, nil +} + +func (store *store) CreateService(ctx context.Context, service *cloudintegrationtypes.StorableCloudIntegrationService) error { + _, err := store. + store. + BunDBCtx(ctx). + NewInsert(). + Model(service). + Exec(ctx) + if err != nil { + return store.store.WrapAlreadyExistsErrf(err, cloudintegrationtypes.ErrCodeCloudIntegrationServiceAlreadyExists, "cloud integration service with id %s already exists for integration account", service.Type) + } + + return nil +} + +func (store *store) UpdateService(ctx context.Context, service *cloudintegrationtypes.StorableCloudIntegrationService) error { + _, err := store. + store. + BunDBCtx(ctx). + NewUpdate(). + Model(service). + WherePK(). + Where("cloud_integration_id = ?", service.CloudIntegrationID). + Where("type = ?", service.Type). + Exec(ctx) + return err +} diff --git a/pkg/types/cloudintegrationtypes/cloudintegration.go b/pkg/types/cloudintegrationtypes/cloudintegration.go index 2b9fb216221..48de78cbcdc 100644 --- a/pkg/types/cloudintegrationtypes/cloudintegration.go +++ b/pkg/types/cloudintegrationtypes/cloudintegration.go @@ -13,7 +13,10 @@ import ( ) var ( - ErrCodeCloudIntegrationNotFound = errors.MustNewCode("cloud_integration_not_found") + ErrCodeCloudIntegrationNotFound = errors.MustNewCode("cloud_integration_not_found") + ErrCodeCloudIntegrationAlreadyExists = errors.MustNewCode("cloud_integration_already_exists") + ErrCodeCloudIntegrationServiceNotFound = errors.MustNewCode("cloud_integration_service_not_found") + ErrCodeCloudIntegrationServiceAlreadyExists = errors.MustNewCode("cloud_integration_service_already_exists") ) // StorableCloudIntegration represents a cloud integration stored in the database. diff --git a/pkg/types/cloudintegrationtypes/store.go b/pkg/types/cloudintegrationtypes/store.go index 576492bc15d..ecc6db01f5d 100644 --- a/pkg/types/cloudintegrationtypes/store.go +++ b/pkg/types/cloudintegrationtypes/store.go @@ -10,8 +10,14 @@ type Store interface { // GetAccountByID returns a cloud integration account by id GetAccountByID(ctx context.Context, orgID, id valuer.UUID, provider CloudProviderType) (*StorableCloudIntegration, error) + // GetConnectedAccount for a given provider + GetConnectedAccount(ctx context.Context, orgID valuer.UUID, provider CloudProviderType, providerAccountID string) (*StorableCloudIntegration, error) + + // ListConnectedAccounts returns all the cloud integration accounts for the org and cloud provider + ListConnectedAccounts(ctx context.Context, orgID valuer.UUID, provider CloudProviderType) ([]*StorableCloudIntegration, error) + // CreateAccount creates a new cloud integration account - CreateAccount(ctx context.Context, account *StorableCloudIntegration) (*StorableCloudIntegration, error) + CreateAccount(ctx context.Context, account *StorableCloudIntegration) error // UpdateAccount updates an existing cloud integration account UpdateAccount(ctx context.Context, account *StorableCloudIntegration) error @@ -19,23 +25,17 @@ type Store interface { // RemoveAccount marks a cloud integration account as removed by setting the RemovedAt field RemoveAccount(ctx context.Context, orgID, id valuer.UUID, provider CloudProviderType) error - // ListConnectedAccounts returns all the cloud integration accounts for the org and cloud provider - ListConnectedAccounts(ctx context.Context, orgID valuer.UUID, provider CloudProviderType) ([]*StorableCloudIntegration, error) - - // GetConnectedAccount for a given provider - GetConnectedAccount(ctx context.Context, orgID valuer.UUID, provider CloudProviderType, providerAccountID string) (*StorableCloudIntegration, error) - // cloud_integration_service related methods // GetServiceByServiceID returns the cloud integration service for the given cloud integration id and service id GetServiceByServiceID(ctx context.Context, cloudIntegrationID valuer.UUID, serviceID ServiceID) (*StorableCloudIntegrationService, error) + // ListServices returns all the cloud integration services for the given cloud integration id + ListServices(ctx context.Context, cloudIntegrationID valuer.UUID) ([]*StorableCloudIntegrationService, error) + // CreateService creates a new cloud integration service - CreateService(ctx context.Context, service *StorableCloudIntegrationService) (*StorableCloudIntegrationService, error) + CreateService(ctx context.Context, service *StorableCloudIntegrationService) error // UpdateService updates an existing cloud integration service UpdateService(ctx context.Context, service *StorableCloudIntegrationService) error - - // ListServices returns all the cloud integration services for the given cloud integration id - ListServices(ctx context.Context, cloudIntegrationID valuer.UUID) ([]*StorableCloudIntegrationService, error) } From 79b4c2e4b0fe6acfb22b7dcc0d86b6c14c5841d3 Mon Sep 17 00:00:00 2001 From: Tushar Vats Date: Mon, 23 Mar 2026 20:19:18 +0530 Subject: [PATCH 13/78] fix: added download button in trace page (#10613) * fix: added download button in trace page * fix: update unit tests * fix: revert prepareQueryRangePayloadV5.ts * fix: addressed comments * fix: address commnets from aditya * fix: update tests --- .../src/api/v1/download/downloadExportData.ts | 37 +- .../DownloadOptionsMenu.styles.scss} | 28 +- .../DownloadOptionsMenu.test.tsx | 331 +++++++++++++++++ .../DownloadOptionsMenu.tsx} | 93 ++--- .../constants.ts | 0 .../LogsDownloadOptionsMenu.test.tsx | 341 ------------------ .../LogsActionsContainer.tsx | 14 +- .../src/container/LogsExplorerViews/index.tsx | 2 - .../tests/LogsExplorerViews.test.tsx | 2 +- .../ListView/ListView.styles.scss | 1 + .../TracesExplorer/ListView/index.tsx | 3 + .../useDownloadOptionsMenu.ts | 95 +++++ .../api/exportRawData/getExportRawData.ts | 10 +- 13 files changed, 489 insertions(+), 468 deletions(-) rename frontend/src/components/{LogsDownloadOptionsMenu/LogsDownloadOptionsMenu.styles.scss => DownloadOptionsMenu/DownloadOptionsMenu.styles.scss} (71%) create mode 100644 frontend/src/components/DownloadOptionsMenu/DownloadOptionsMenu.test.tsx rename frontend/src/components/{LogsDownloadOptionsMenu/LogsDownloadOptionsMenu.tsx => DownloadOptionsMenu/DownloadOptionsMenu.tsx} (57%) rename frontend/src/components/{LogsDownloadOptionsMenu => DownloadOptionsMenu}/constants.ts (100%) delete mode 100644 frontend/src/components/LogsDownloadOptionsMenu/LogsDownloadOptionsMenu.test.tsx create mode 100644 frontend/src/hooks/useDownloadOptionsMenu/useDownloadOptionsMenu.ts diff --git a/frontend/src/api/v1/download/downloadExportData.ts b/frontend/src/api/v1/download/downloadExportData.ts index 30bc7b25dd7..73004b5a0fd 100644 --- a/frontend/src/api/v1/download/downloadExportData.ts +++ b/frontend/src/api/v1/download/downloadExportData.ts @@ -8,42 +8,32 @@ export const downloadExportData = async ( props: ExportRawDataProps, ): Promise => { try { - const queryParams = new URLSearchParams(); - - queryParams.append('start', String(props.start)); - queryParams.append('end', String(props.end)); - queryParams.append('filter', props.filter); - props.columns.forEach((col) => { - queryParams.append('columns', col); - }); - queryParams.append('order_by', props.orderBy); - queryParams.append('limit', String(props.limit)); - queryParams.append('format', props.format); - - const response = await axios.get(`export_raw_data?${queryParams}`, { - responseType: 'blob', // Important: tell axios to handle response as blob - decompress: true, // Enable automatic decompression - headers: { - Accept: 'application/octet-stream', // Tell server we expect binary data + const response = await axios.post( + `export_raw_data?format=${encodeURIComponent(props.format)}`, + props.body, + { + responseType: 'blob', + decompress: true, + headers: { + Accept: 'application/octet-stream', + 'Content-Type': 'application/json', + }, + timeout: 0, }, - timeout: 0, - }); + ); - // Only proceed if the response status is 200 if (response.status !== 200) { throw new Error( `Failed to download data: server returned status ${response.status}`, ); } - // Create blob URL from response data + const blob = new Blob([response.data], { type: 'application/octet-stream' }); const url = window.URL.createObjectURL(blob); - // Create and configure download link const link = document.createElement('a'); link.href = url; - // Get filename from Content-Disposition header or generate timestamped default const filename = response.headers['content-disposition'] ?.split('filename=')[1] @@ -51,7 +41,6 @@ export const downloadExportData = async ( link.setAttribute('download', filename); - // Trigger download document.body.appendChild(link); link.click(); link.remove(); diff --git a/frontend/src/components/LogsDownloadOptionsMenu/LogsDownloadOptionsMenu.styles.scss b/frontend/src/components/DownloadOptionsMenu/DownloadOptionsMenu.styles.scss similarity index 71% rename from frontend/src/components/LogsDownloadOptionsMenu/LogsDownloadOptionsMenu.styles.scss rename to frontend/src/components/DownloadOptionsMenu/DownloadOptionsMenu.styles.scss index e9f2d92df37..ca9f81e5c33 100644 --- a/frontend/src/components/LogsDownloadOptionsMenu/LogsDownloadOptionsMenu.styles.scss +++ b/frontend/src/components/DownloadOptionsMenu/DownloadOptionsMenu.styles.scss @@ -1,11 +1,11 @@ -.logs-download-popover { +.download-popover { .ant-popover-inner { border-radius: 4px; - border: 1px solid var(--bg-slate-400); + border: 1px solid var(--l3-border); background: linear-gradient( 139deg, - var(--bg-ink-400) 0%, - var(--bg-ink-500) 98.68% + var(--l2-background) 0%, + var(--l3-background) 98.68% ); box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2); backdrop-filter: blur(20px); @@ -19,7 +19,7 @@ .title { display: flex; - color: var(--bg-slate-50); + color: var(--l3-foreground); font-family: Inter; font-size: 11px; font-style: normal; @@ -38,7 +38,7 @@ flex-direction: column; :global(.ant-radio-wrapper) { - color: var(--bg-vanilla-400); + color: var(--foreground); font-family: Inter; font-size: 13px; } @@ -46,7 +46,7 @@ .horizontal-line { height: 1px; - background: var(--bg-slate-400); + background: var(--l3-border); } .export-button { @@ -59,27 +59,27 @@ } .lightMode { - .logs-download-popover { + .download-popover { .ant-popover-inner { - border: 1px solid var(--bg-vanilla-300); + border: 1px solid var(--l2-border); background: linear-gradient( 139deg, - var(--bg-vanilla-100) 0%, - var(--bg-vanilla-300) 98.68% + var(--background) 0%, + var(--l1-background) 98.68% ); box-shadow: 4px 10px 16px 2px rgba(255, 255, 255, 0.2); } .export-options-container { .title { - color: var(--bg-ink-200); + color: var(--l2-foreground); } :global(.ant-radio-wrapper) { - color: var(--bg-ink-400); + color: var(--foreground); } .horizontal-line { - background: var(--bg-vanilla-300); + background: var(--l2-border); } } } diff --git a/frontend/src/components/DownloadOptionsMenu/DownloadOptionsMenu.test.tsx b/frontend/src/components/DownloadOptionsMenu/DownloadOptionsMenu.test.tsx new file mode 100644 index 00000000000..6de2dfc1be1 --- /dev/null +++ b/frontend/src/components/DownloadOptionsMenu/DownloadOptionsMenu.test.tsx @@ -0,0 +1,331 @@ +// eslint-disable-next-line no-restricted-imports +import { Provider } from 'react-redux'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { message } from 'antd'; +import configureStore from 'redux-mock-store'; +import store from 'store'; +import { Query } from 'types/api/queryBuilder/queryBuilderData'; +import { EQueryType } from 'types/common/dashboard'; +import { DataSource, StringOperators } from 'types/common/queryBuilder'; + +import '@testing-library/jest-dom'; + +import { DownloadFormats, DownloadRowCounts } from './constants'; +import DownloadOptionsMenu from './DownloadOptionsMenu'; + +const mockDownloadExportData = jest.fn().mockResolvedValue(undefined); +jest.mock('api/v1/download/downloadExportData', () => ({ + downloadExportData: (...args: any[]): any => mockDownloadExportData(...args), + default: (...args: any[]): any => mockDownloadExportData(...args), +})); + +jest.mock('antd', () => { + const actual = jest.requireActual('antd'); + return { + ...actual, + message: { + success: jest.fn(), + error: jest.fn(), + }, + }; +}); + +const mockUseQueryBuilder = jest.fn(); +jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({ + useQueryBuilder: (): any => mockUseQueryBuilder(), +})); + +const mockStore = configureStore([]); +const createMockReduxStore = (): any => + mockStore({ + ...store.getState(), + }); + +const createMockStagedQuery = (dataSource: DataSource): Query => ({ + id: 'test-query-id', + queryType: EQueryType.QUERY_BUILDER, + builder: { + queryData: [ + { + queryName: 'A', + dataSource, + aggregateOperator: StringOperators.NOOP, + aggregateAttribute: { + id: '', + dataType: '' as any, + key: '', + type: '', + }, + aggregations: [{ expression: 'count()' }], + functions: [], + filter: { expression: 'status = 200' }, + filters: { items: [], op: 'AND' }, + groupBy: [], + expression: 'A', + disabled: false, + having: { expression: '' } as any, + limit: null, + stepInterval: null, + orderBy: [{ columnName: 'timestamp', order: 'desc' }], + legend: '', + selectColumns: [], + }, + ], + queryFormulas: [], + queryTraceOperator: [], + }, + promql: [], + clickhouse_sql: [], +}); + +const renderWithStore = (dataSource: DataSource): void => { + const mockReduxStore = createMockReduxStore(); + render( + + + , + ); +}; + +describe.each([ + [DataSource.LOGS, 'logs'], + [DataSource.TRACES, 'traces'], +])('DownloadOptionsMenu for %s', (dataSource, signal) => { + const testId = `periscope-btn-download-${dataSource}`; + + beforeEach(() => { + mockDownloadExportData.mockReset().mockResolvedValue(undefined); + (message.success as jest.Mock).mockReset(); + (message.error as jest.Mock).mockReset(); + mockUseQueryBuilder.mockReturnValue({ + stagedQuery: createMockStagedQuery(dataSource), + }); + }); + + it('renders download button', () => { + renderWithStore(dataSource); + const button = screen.getByTestId(testId); + expect(button).toBeInTheDocument(); + expect(button).toHaveClass('periscope-btn', 'ghost'); + }); + + it('shows popover with export options when download button is clicked', () => { + renderWithStore(dataSource); + fireEvent.click(screen.getByTestId(testId)); + + expect(screen.getByRole('dialog')).toBeInTheDocument(); + expect(screen.getByText('FORMAT')).toBeInTheDocument(); + expect(screen.getByText('Number of Rows')).toBeInTheDocument(); + expect(screen.getByText('Columns')).toBeInTheDocument(); + }); + + it('allows changing export format', () => { + renderWithStore(dataSource); + fireEvent.click(screen.getByTestId(testId)); + + const csvRadio = screen.getByRole('radio', { name: 'csv' }); + const jsonlRadio = screen.getByRole('radio', { name: 'jsonl' }); + + expect(csvRadio).toBeChecked(); + fireEvent.click(jsonlRadio); + expect(jsonlRadio).toBeChecked(); + expect(csvRadio).not.toBeChecked(); + }); + + it('allows changing row limit', () => { + renderWithStore(dataSource); + fireEvent.click(screen.getByTestId(testId)); + + const tenKRadio = screen.getByRole('radio', { name: '10k' }); + const fiftyKRadio = screen.getByRole('radio', { name: '50k' }); + + expect(tenKRadio).toBeChecked(); + fireEvent.click(fiftyKRadio); + expect(fiftyKRadio).toBeChecked(); + expect(tenKRadio).not.toBeChecked(); + }); + + it('allows changing columns scope', () => { + renderWithStore(dataSource); + fireEvent.click(screen.getByTestId(testId)); + + const allColumnsRadio = screen.getByRole('radio', { name: 'All' }); + const selectedColumnsRadio = screen.getByRole('radio', { name: 'Selected' }); + + expect(allColumnsRadio).toBeChecked(); + fireEvent.click(selectedColumnsRadio); + expect(selectedColumnsRadio).toBeChecked(); + expect(allColumnsRadio).not.toBeChecked(); + }); + + it('calls downloadExportData with correct format and POST body', async () => { + renderWithStore(dataSource); + fireEvent.click(screen.getByTestId(testId)); + fireEvent.click(screen.getByText('Export')); + + await waitFor(() => { + expect(mockDownloadExportData).toHaveBeenCalledTimes(1); + const callArgs = mockDownloadExportData.mock.calls[0][0]; + expect(callArgs.format).toBe(DownloadFormats.CSV); + expect(callArgs.body).toBeDefined(); + expect(callArgs.body.requestType).toBe('raw'); + expect(callArgs.body.compositeQuery.queries).toHaveLength(1); + + const query = callArgs.body.compositeQuery.queries[0]; + expect(query.type).toBe('builder_query'); + expect(query.spec.signal).toBe(signal); + expect(query.spec.limit).toBe(DownloadRowCounts.TEN_K); + }); + }); + + it('clears groupBy and having in the export payload', async () => { + const mockQuery = createMockStagedQuery(dataSource); + mockQuery.builder.queryData[0].groupBy = [ + { key: 'service', dataType: 'string' as any, type: '' }, + ]; + mockQuery.builder.queryData[0].having = { + expression: 'count() > 10', + } as any; + + mockUseQueryBuilder.mockReturnValue({ stagedQuery: mockQuery }); + renderWithStore(dataSource); + fireEvent.click(screen.getByTestId(testId)); + fireEvent.click(screen.getByText('Export')); + + await waitFor(() => { + expect(mockDownloadExportData).toHaveBeenCalledTimes(1); + const callArgs = mockDownloadExportData.mock.calls[0][0]; + const query = callArgs.body.compositeQuery.queries[0]; + expect(query.spec.groupBy).toBeUndefined(); + expect(query.spec.having).toEqual({ expression: '' }); + }); + }); + + it('keeps selectColumns when column scope is Selected', async () => { + const mockQuery = createMockStagedQuery(dataSource); + mockQuery.builder.queryData[0].selectColumns = [ + { name: 'http.status', fieldDataType: 'int64', fieldContext: 'attribute' }, + ] as any; + + mockUseQueryBuilder.mockReturnValue({ stagedQuery: mockQuery }); + renderWithStore(dataSource); + fireEvent.click(screen.getByTestId(testId)); + fireEvent.click(screen.getByRole('radio', { name: 'Selected' })); + fireEvent.click(screen.getByText('Export')); + + await waitFor(() => { + expect(mockDownloadExportData).toHaveBeenCalledTimes(1); + const callArgs = mockDownloadExportData.mock.calls[0][0]; + const query = callArgs.body.compositeQuery.queries[0]; + expect(query.spec.selectFields).toEqual([ + expect.objectContaining({ + name: 'http.status', + fieldDataType: 'int64', + }), + ]); + }); + }); + + it('sends no selectFields when column scope is All', async () => { + renderWithStore(dataSource); + fireEvent.click(screen.getByTestId(testId)); + fireEvent.click(screen.getByRole('radio', { name: 'All' })); + fireEvent.click(screen.getByText('Export')); + + await waitFor(() => { + expect(mockDownloadExportData).toHaveBeenCalledTimes(1); + const callArgs = mockDownloadExportData.mock.calls[0][0]; + const query = callArgs.body.compositeQuery.queries[0]; + expect(query.spec.selectFields).toBeUndefined(); + }); + }); + + it('handles successful export with success message', async () => { + renderWithStore(dataSource); + fireEvent.click(screen.getByTestId(testId)); + fireEvent.click(screen.getByText('Export')); + + await waitFor(() => { + expect(message.success).toHaveBeenCalledWith( + 'Export completed successfully', + ); + }); + }); + + it('handles export failure with error message', async () => { + mockDownloadExportData.mockRejectedValueOnce(new Error('Server error')); + renderWithStore(dataSource); + fireEvent.click(screen.getByTestId(testId)); + fireEvent.click(screen.getByText('Export')); + + await waitFor(() => { + expect(message.error).toHaveBeenCalledWith( + `Failed to export ${dataSource}. Please try again.`, + ); + }); + }); + + it('handles UI state correctly during export process', async () => { + let resolveDownload: () => void; + mockDownloadExportData.mockImplementationOnce( + () => + new Promise((resolve) => { + resolveDownload = resolve; + }), + ); + renderWithStore(dataSource); + + fireEvent.click(screen.getByTestId(testId)); + expect(screen.getByRole('dialog')).toBeInTheDocument(); + + fireEvent.click(screen.getByText('Export')); + + expect(screen.getByTestId(testId)).toBeDisabled(); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + + resolveDownload!(); + + await waitFor(() => { + expect(screen.getByTestId(testId)).not.toBeDisabled(); + }); + }); +}); + +describe('DownloadOptionsMenu for traces with queryTraceOperator', () => { + const dataSource = DataSource.TRACES; + const testId = `periscope-btn-download-${dataSource}`; + + beforeEach(() => { + mockDownloadExportData.mockReset().mockResolvedValue(undefined); + (message.success as jest.Mock).mockReset(); + }); + + it('applies limit and clears groupBy on queryTraceOperator entries', async () => { + const query = createMockStagedQuery(dataSource); + query.builder.queryTraceOperator = [ + { + ...query.builder.queryData[0], + queryName: 'TraceOp1', + expression: 'TraceOp1', + groupBy: [{ key: 'service', dataType: 'string' as any, type: '' }], + }, + ]; + + mockUseQueryBuilder.mockReturnValue({ stagedQuery: query }); + renderWithStore(dataSource); + fireEvent.click(screen.getByTestId(testId)); + fireEvent.click(screen.getByRole('radio', { name: '50k' })); + fireEvent.click(screen.getByText('Export')); + + await waitFor(() => { + expect(mockDownloadExportData).toHaveBeenCalledTimes(1); + const callArgs = mockDownloadExportData.mock.calls[0][0]; + const queries = callArgs.body.compositeQuery.queries; + const traceOpQuery = queries.find((q: any) => q.spec.name === 'TraceOp1'); + if (traceOpQuery) { + expect(traceOpQuery.spec.limit).toBe(DownloadRowCounts.FIFTY_K); + expect(traceOpQuery.spec.groupBy).toBeUndefined(); + } + }); + }); +}); diff --git a/frontend/src/components/LogsDownloadOptionsMenu/LogsDownloadOptionsMenu.tsx b/frontend/src/components/DownloadOptionsMenu/DownloadOptionsMenu.tsx similarity index 57% rename from frontend/src/components/LogsDownloadOptionsMenu/LogsDownloadOptionsMenu.tsx rename to frontend/src/components/DownloadOptionsMenu/DownloadOptionsMenu.tsx index 5951fb6544a..7b9d5227197 100644 --- a/frontend/src/components/LogsDownloadOptionsMenu/LogsDownloadOptionsMenu.tsx +++ b/frontend/src/components/DownloadOptionsMenu/DownloadOptionsMenu.tsx @@ -1,8 +1,8 @@ import { useCallback, useMemo, useState } from 'react'; -import { Button, message, Popover, Radio, Tooltip, Typography } from 'antd'; -import { downloadExportData } from 'api/v1/download/downloadExportData'; +import { Button, Popover, Radio, Tooltip, Typography } from 'antd'; +import { useExportRawData } from 'hooks/useDownloadOptionsMenu/useDownloadOptionsMenu'; import { Download, DownloadIcon, Loader2 } from 'lucide-react'; -import { TelemetryFieldKey } from 'types/api/v5/queryRange'; +import { DataSource } from 'types/common/queryBuilder'; import { DownloadColumnsScopes, @@ -10,75 +10,34 @@ import { DownloadRowCounts, } from './constants'; -import './LogsDownloadOptionsMenu.styles.scss'; +import './DownloadOptionsMenu.styles.scss'; -function convertTelemetryFieldKeyToText(key: TelemetryFieldKey): string { - const prefix = key.fieldContext ? `${key.fieldContext}.` : ''; - const suffix = key.fieldDataType ? `:${key.fieldDataType}` : ''; - return `${prefix}${key.name}${suffix}`; +interface DownloadOptionsMenuProps { + dataSource: DataSource; } -interface LogsDownloadOptionsMenuProps { - startTime: number; - endTime: number; - filter: string; - columns: TelemetryFieldKey[]; - orderBy: string; -} - -export default function LogsDownloadOptionsMenu({ - startTime, - endTime, - filter, - columns, - orderBy, -}: LogsDownloadOptionsMenuProps): JSX.Element { +export default function DownloadOptionsMenu({ + dataSource, +}: DownloadOptionsMenuProps): JSX.Element { const [exportFormat, setExportFormat] = useState(DownloadFormats.CSV); const [rowLimit, setRowLimit] = useState(DownloadRowCounts.TEN_K); const [columnsScope, setColumnsScope] = useState( DownloadColumnsScopes.ALL, ); - const [isDownloading, setIsDownloading] = useState(false); const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const handleExportRawData = useCallback(async (): Promise => { - setIsPopoverOpen(false); - try { - setIsDownloading(true); - const downloadOptions = { - source: 'logs', - start: startTime, - end: endTime, - columns: - columnsScope === DownloadColumnsScopes.SELECTED - ? columns.map((col) => convertTelemetryFieldKeyToText(col)) - : [], - filter, - orderBy, - format: exportFormat, - limit: rowLimit, - }; + const { isDownloading, handleExportRawData } = useExportRawData({ + dataSource, + }); - await downloadExportData(downloadOptions); - message.success('Export completed successfully'); - } catch (error) { - console.error('Error exporting logs:', error); - message.error('Failed to export logs. Please try again.'); - } finally { - setIsDownloading(false); - } - }, [ - startTime, - endTime, - columnsScope, - columns, - filter, - orderBy, - exportFormat, - rowLimit, - setIsDownloading, - setIsPopoverOpen, - ]); + const handleExport = useCallback(async (): Promise => { + setIsPopoverOpen(false); + await handleExportRawData({ + format: exportFormat, + rowLimit, + clearSelectColumns: columnsScope === DownloadColumnsScopes.ALL, + }); + }, [exportFormat, rowLimit, columnsScope, handleExportRawData]); const popoverContent = useMemo( () => ( @@ -129,7 +88,7 @@ export default function LogsDownloadOptionsMenu({ ), - [exportFormat, rowLimit, columnsScope, isDownloading, handleExportRawData], + [exportFormat, rowLimit, columnsScope, isDownloading, handleExport], ); return ( @@ -149,19 +108,19 @@ export default function LogsDownloadOptionsMenu({ arrow={false} open={isPopoverOpen} onOpenChange={setIsPopoverOpen} - rootClassName="logs-download-popover" + rootClassName="download-popover" > @@ -384,17 +395,16 @@ export default function Onboarding(): JSX.Element { {activeStep > 1 && (
{ + onReselectModule={(e?: React.MouseEvent): void => { setCurrent(current - 1); setActiveStep(activeStep - 1); setSelectedModule(useCases.APM); resetProgress(); - if (isOnboardingV3Enabled) { - history.push(ROUTES.GET_STARTED_WITH_CLOUD); - } else { - history.push(ROUTES.GET_STARTED); - } + const path = isOnboardingV3Enabled + ? ROUTES.GET_STARTED_WITH_CLOUD + : ROUTES.GET_STARTED; + safeNavigate(path, { newTab: !!e && isModifierKeyPressed(e) }); }} selectedModule={selectedModule} selectedModuleSteps={selectedModuleSteps} diff --git a/frontend/src/container/OnboardingContainer/Steps/DataSource/DataSource.tsx b/frontend/src/container/OnboardingContainer/Steps/DataSource/DataSource.tsx index 3d88e36516e..b2554568714 100644 --- a/frontend/src/container/OnboardingContainer/Steps/DataSource/DataSource.tsx +++ b/frontend/src/container/OnboardingContainer/Steps/DataSource/DataSource.tsx @@ -1,6 +1,5 @@ -import { useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useHistory } from 'react-router-dom'; import { LoadingOutlined } from '@ant-design/icons'; import { Button, Card, Form, Input, Select, Space, Typography } from 'antd'; import logEvent from 'api/common/logEvent'; @@ -19,8 +18,10 @@ import { messagingQueueKakfaSupportedDataSources, } from 'container/OnboardingContainer/utils/dataSourceUtils'; import { useNotifications } from 'hooks/useNotifications'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; import useUrlQuery from 'hooks/useUrlQuery'; import { Blocks, Check } from 'lucide-react'; +import { isModifierKeyPressed } from 'utils/app'; import { popupContainer } from 'utils/selectPopupContainer'; import './DataSource.styles.scss'; @@ -35,7 +36,7 @@ export interface DataSourceType { export default function DataSource(): JSX.Element { const [form] = Form.useForm(); const { t } = useTranslation(['common']); - const history = useHistory(); + const { safeNavigate } = useSafeNavigate(); const getStartedSource = useUrlQuery().get(QueryParams.getStartedSource); @@ -139,13 +140,13 @@ export default function DataSource(): JSX.Element { } }; - const goToIntegrationsPage = (): void => { + const goToIntegrationsPage = (e?: React.MouseEvent): void => { logEvent('Onboarding V2: Go to integrations', { module: selectedModule?.id, dataSource: selectedDataSource?.name, framework: selectedFramework, }); - history.push(ROUTES.INTEGRATIONS); + safeNavigate(ROUTES.INTEGRATIONS, { newTab: !!e && isModifierKeyPressed(e) }); }; return ( @@ -247,7 +248,7 @@ export default function DataSource(): JSX.Element { page which allows more sources of sending data @@ -458,7 +462,11 @@ export default function ModuleStepsContainer({ > Back - (allGroupedDataSources); @@ -413,7 +415,10 @@ function OnboardingAddDataSource(): JSX.Element { ]); }, [org]); - const handleUpdateCurrentStep = (step: number): void => { + const handleUpdateCurrentStep = ( + step: number, + event?: React.MouseEvent, + ): void => { setCurrentStep(step); if (step === 1) { @@ -443,43 +448,45 @@ function OnboardingAddDataSource(): JSX.Element { ...setupStepItemsBase.slice(2), ]); } else if (step === 3) { + let targetPath: string; switch (selectedDataSource?.module) { case 'apm': - history.push(ROUTES.APPLICATION); + targetPath = ROUTES.APPLICATION; break; case 'logs': - history.push(ROUTES.LOGS); + targetPath = ROUTES.LOGS; break; case 'metrics': - history.push(ROUTES.METRICS_EXPLORER); + targetPath = ROUTES.METRICS_EXPLORER; break; case 'dashboards': - history.push(ROUTES.ALL_DASHBOARD); + targetPath = ROUTES.ALL_DASHBOARD; break; case 'infra-monitoring-hosts': - history.push(ROUTES.INFRASTRUCTURE_MONITORING_HOSTS); + targetPath = ROUTES.INFRASTRUCTURE_MONITORING_HOSTS; break; case 'infra-monitoring-k8s': - history.push(ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES); + targetPath = ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES; break; case 'messaging-queues-kafka': - history.push(ROUTES.MESSAGING_QUEUES_KAFKA); + targetPath = ROUTES.MESSAGING_QUEUES_KAFKA; break; case 'messaging-queues-celery': - history.push(ROUTES.MESSAGING_QUEUES_CELERY_TASK); + targetPath = ROUTES.MESSAGING_QUEUES_CELERY_TASK; break; case 'integrations': - history.push(ROUTES.INTEGRATIONS); + targetPath = ROUTES.INTEGRATIONS; break; case 'home': - history.push(ROUTES.HOME); + targetPath = ROUTES.HOME; break; case 'api-monitoring': - history.push(ROUTES.API_MONITORING); + targetPath = ROUTES.API_MONITORING; break; default: - history.push(ROUTES.APPLICATION); + targetPath = ROUTES.APPLICATION; } + safeNavigate(targetPath, { newTab: !!event && isModifierKeyPressed(event) }); } }; @@ -633,7 +640,7 @@ function OnboardingAddDataSource(): JSX.Element { { + onClick={(e): void => { logEvent( `${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.CLOSE_ONBOARDING_CLICKED}`, { @@ -641,7 +648,7 @@ function OnboardingAddDataSource(): JSX.Element { }, ); - history.push(ROUTES.HOME); + safeNavigate(ROUTES.HOME, { newTab: isModifierKeyPressed(e) }); }} /> {progressText} @@ -968,7 +975,7 @@ function OnboardingAddDataSource(): JSX.Element { type="primary" disabled={!selectedDataSource} shape="round" - onClick={(): void => { + onClick={(e): void => { logEvent( `${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.CONFIGURED_PRODUCT}`, { @@ -982,7 +989,9 @@ function OnboardingAddDataSource(): JSX.Element { selectedEnvironment || selectedFramework || selectedDataSource; if (currentEntity?.internalRedirect && currentEntity?.link) { - history.push(currentEntity.link); + safeNavigate(currentEntity.link, { + newTab: isModifierKeyPressed(e), + }); } else { handleUpdateCurrentStep(2); } @@ -1053,7 +1062,7 @@ function OnboardingAddDataSource(): JSX.Element { diff --git a/frontend/src/utils/__tests__/app.test.ts b/frontend/src/utils/__tests__/app.test.ts index c1edbc5ff79..a2f82f0ed3c 100644 --- a/frontend/src/utils/__tests__/app.test.ts +++ b/frontend/src/utils/__tests__/app.test.ts @@ -80,6 +80,15 @@ describe('buildAbsolutePath', () => { expect(result).toBe(`${BASE_PATH}/?search=test`); }); + it('should preserve pathname without adding trailing slash for empty relative path', () => { + mockLocation(`${BASE_PATH}`); + const result = buildAbsolutePath({ + relativePath: '', + urlQueryString: 'search=test', + }); + expect(result).toBe(`${BASE_PATH}?search=test`); + }); + it('should handle relative path starting with forward slash', () => { mockLocation(`${BASE_PATH}/`); const result = buildAbsolutePath({ relativePath: '/users' }); diff --git a/frontend/src/utils/__tests__/navigation.test.ts b/frontend/src/utils/__tests__/navigation.test.ts new file mode 100644 index 00000000000..adfce32d263 --- /dev/null +++ b/frontend/src/utils/__tests__/navigation.test.ts @@ -0,0 +1,80 @@ +import { isModifierKeyPressed } from '../app'; +import { openInNewTab } from '../navigation'; + +describe('navigation utilities', () => { + const originalWindowOpen = window.open; + + beforeEach(() => { + window.open = jest.fn(); + }); + + afterEach(() => { + window.open = originalWindowOpen; + }); + + describe('isModifierKeyPressed', () => { + const createMouseEvent = (overrides: Partial = {}): MouseEvent => + ({ + metaKey: false, + ctrlKey: false, + button: 0, + ...overrides, + } as MouseEvent); + + it('returns true when metaKey is pressed (Cmd on Mac)', () => { + const event = createMouseEvent({ metaKey: true }); + expect(isModifierKeyPressed(event)).toBe(true); + }); + + it('returns true when ctrlKey is pressed (Ctrl on Windows/Linux)', () => { + const event = createMouseEvent({ ctrlKey: true }); + expect(isModifierKeyPressed(event)).toBe(true); + }); + + it('returns true when both metaKey and ctrlKey are pressed', () => { + const event = createMouseEvent({ metaKey: true, ctrlKey: true }); + expect(isModifierKeyPressed(event)).toBe(true); + }); + + it('returns false when neither modifier key is pressed', () => { + const event = createMouseEvent(); + expect(isModifierKeyPressed(event)).toBe(false); + }); + + it('returns false when only shiftKey or altKey are pressed', () => { + const event = createMouseEvent({ + shiftKey: true, + altKey: true, + } as Partial); + expect(isModifierKeyPressed(event)).toBe(false); + }); + + it('returns true when middle mouse button is used', () => { + const event = createMouseEvent({ button: 1 }); + expect(isModifierKeyPressed(event)).toBe(true); + }); + }); + + describe('openInNewTab', () => { + it('calls window.open with the given path and _blank target', () => { + openInNewTab('/dashboard'); + expect(window.open).toHaveBeenCalledWith('/dashboard', '_blank'); + }); + + it('handles full URLs', () => { + openInNewTab('https://example.com/page'); + expect(window.open).toHaveBeenCalledWith( + 'https://example.com/page', + '_blank', + ); + }); + + it('handles paths with query strings', () => { + openInNewTab('/alerts?tab=AlertRules&relativeTime=30m'); + expect(window.open).toHaveBeenCalledWith( + '/alerts?tab=AlertRules&relativeTime=30m', + '_blank', + ); + }); + }); +}); diff --git a/frontend/src/utils/app.ts b/frontend/src/utils/app.ts index a1c74aea08f..61fc72719ac 100644 --- a/frontend/src/utils/app.ts +++ b/frontend/src/utils/app.ts @@ -60,6 +60,11 @@ export function buildAbsolutePath({ urlQueryString?: string; }): string { const { pathname } = getLocation(); + + if (!relativePath) { + return urlQueryString ? `${pathname}?${urlQueryString}` : pathname; + } + // ensure base path always ends with a forward slash const basePath = pathname.endsWith('/') ? pathname : `${pathname}/`; @@ -75,6 +80,15 @@ export function buildAbsolutePath({ export const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; +/** + * Returns true if the user is holding Cmd (Mac) or Ctrl (Windows/Linux) + * during a click event, or if the middle mouse button is used — + * the universal "open in new tab" modifiers. + */ +export const isModifierKeyPressed = ( + event: MouseEvent | React.MouseEvent, +): boolean => event.metaKey || event.ctrlKey || event.button === 1; + export function toISOString( date: Date | string | number | null | undefined, ): string | null { diff --git a/frontend/src/utils/navigation.ts b/frontend/src/utils/navigation.ts new file mode 100644 index 00000000000..f8fef287659 --- /dev/null +++ b/frontend/src/utils/navigation.ts @@ -0,0 +1,6 @@ +/** + * Opens the given path in a new browser tab. + */ +export const openInNewTab = (path: string): void => { + window.open(path, '_blank'); +}; From bb4e7df68bf277265b9a2c0e743b25bae3bc1397 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Mon, 30 Mar 2026 16:08:52 +0530 Subject: [PATCH 47/78] chore: add rule state history module (#10488) * chore: add rule state history module * chore: run generate * chore: generate * Fix timeline default limit and escape exists key (#10490) Co-authored-by: Cursor Agent * chore: remove unused AddRuleStateHistory and add comments * chore: regenerate * chore: update names and move functions * chore: remove return * chore: update .github/CODEOWNERS for history * chore: update condition builder * chore: lint --------- Co-authored-by: Cursor Agent --- .github/CODEOWNERS | 4 +- docs/api/openapi.yml | 646 +++++++++++++++ ee/query-service/app/server.go | 39 +- ee/query-service/rules/manager.go | 4 + .../src/api/generated/services/rules/index.ts | 744 ++++++++++++++++++ .../api/generated/services/sigNoz.schemas.ts | 393 +++++++++ pkg/apiserver/signozapiserver/provider.go | 10 + .../signozapiserver/rulestatehistory.go | 118 +++ .../implrulestatehistory/condition_builder.go | 106 +++ .../implrulestatehistory/field_mapper.go | 66 ++ .../implrulestatehistory/handler.go | 321 ++++++++ .../implrulestatehistory/module.go | 179 +++++ .../implrulestatehistory/store.go | 574 ++++++++++++++ .../rulestatehistory/rulestatehistory.go | 61 ++ pkg/query-service/app/server.go | 34 +- pkg/query-service/rules/base_rule.go | 140 ++-- pkg/query-service/rules/manager.go | 5 + .../rules/threshold_rule_test.go | 12 +- pkg/signoz/handler.go | 4 + pkg/signoz/module.go | 76 +- pkg/signoz/openapi.go | 2 + pkg/signoz/provider.go | 1 + pkg/types/rulestatehistorytypes/http.go | 20 + pkg/types/rulestatehistorytypes/query.go | 35 + pkg/types/rulestatehistorytypes/response.go | 49 ++ pkg/types/rulestatehistorytypes/store.go | 124 +++ 26 files changed, 3605 insertions(+), 162 deletions(-) create mode 100644 frontend/src/api/generated/services/rules/index.ts create mode 100644 pkg/apiserver/signozapiserver/rulestatehistory.go create mode 100644 pkg/modules/rulestatehistory/implrulestatehistory/condition_builder.go create mode 100644 pkg/modules/rulestatehistory/implrulestatehistory/field_mapper.go create mode 100644 pkg/modules/rulestatehistory/implrulestatehistory/handler.go create mode 100644 pkg/modules/rulestatehistory/implrulestatehistory/module.go create mode 100644 pkg/modules/rulestatehistory/implrulestatehistory/store.go create mode 100644 pkg/modules/rulestatehistory/rulestatehistory.go create mode 100644 pkg/types/rulestatehistorytypes/http.go create mode 100644 pkg/types/rulestatehistorytypes/query.go create mode 100644 pkg/types/rulestatehistorytypes/response.go create mode 100644 pkg/types/rulestatehistorytypes/store.go diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index eddd744ef57..6d549b1731c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -86,6 +86,8 @@ go.mod @therealpandey /pkg/types/alertmanagertypes @srikanthccv /pkg/alertmanager/ @srikanthccv /pkg/ruler/ @srikanthccv +/pkg/modules/rulestatehistory/ @srikanthccv +/pkg/types/rulestatehistorytypes/ @srikanthccv # Correlation-adjacent @@ -105,7 +107,7 @@ go.mod @therealpandey /pkg/modules/authdomain/ @vikrantgupta25 /pkg/modules/role/ @vikrantgupta25 -# IdentN Owners +# IdentN Owners /pkg/identn/ @vikrantgupta25 /pkg/http/middleware/identn.go @vikrantgupta25 diff --git a/docs/api/openapi.yml b/docs/api/openapi.yml index d58eb59fdf7..d069c995478 100644 --- a/docs/api/openapi.yml +++ b/docs/api/openapi.yml @@ -2279,6 +2279,140 @@ components: - status - error type: object + RulestatehistorytypesAlertState: + enum: + - inactive + - pending + - recovering + - firing + - nodata + - disabled + type: string + RulestatehistorytypesGettableRuleStateHistory: + properties: + fingerprint: + minimum: 0 + type: integer + labels: + items: + $ref: '#/components/schemas/Querybuildertypesv5Label' + nullable: true + type: array + overallState: + $ref: '#/components/schemas/RulestatehistorytypesAlertState' + overallStateChanged: + type: boolean + ruleID: + type: string + ruleName: + type: string + state: + $ref: '#/components/schemas/RulestatehistorytypesAlertState' + stateChanged: + type: boolean + unixMilli: + format: int64 + type: integer + value: + format: double + type: number + required: + - ruleID + - ruleName + - overallState + - overallStateChanged + - state + - stateChanged + - unixMilli + - labels + - fingerprint + - value + type: object + RulestatehistorytypesGettableRuleStateHistoryContributor: + properties: + count: + minimum: 0 + type: integer + fingerprint: + minimum: 0 + type: integer + labels: + items: + $ref: '#/components/schemas/Querybuildertypesv5Label' + nullable: true + type: array + relatedLogsLink: + type: string + relatedTracesLink: + type: string + required: + - fingerprint + - labels + - count + type: object + RulestatehistorytypesGettableRuleStateHistoryStats: + properties: + currentAvgResolutionTime: + format: double + type: number + currentAvgResolutionTimeSeries: + $ref: '#/components/schemas/Querybuildertypesv5TimeSeries' + currentTriggersSeries: + $ref: '#/components/schemas/Querybuildertypesv5TimeSeries' + pastAvgResolutionTime: + format: double + type: number + pastAvgResolutionTimeSeries: + $ref: '#/components/schemas/Querybuildertypesv5TimeSeries' + pastTriggersSeries: + $ref: '#/components/schemas/Querybuildertypesv5TimeSeries' + totalCurrentTriggers: + minimum: 0 + type: integer + totalPastTriggers: + minimum: 0 + type: integer + required: + - totalCurrentTriggers + - totalPastTriggers + - currentTriggersSeries + - pastTriggersSeries + - currentAvgResolutionTime + - pastAvgResolutionTime + - currentAvgResolutionTimeSeries + - pastAvgResolutionTimeSeries + type: object + RulestatehistorytypesGettableRuleStateTimeline: + properties: + items: + items: + $ref: '#/components/schemas/RulestatehistorytypesGettableRuleStateHistory' + nullable: true + type: array + nextCursor: + type: string + total: + minimum: 0 + type: integer + required: + - items + - total + type: object + RulestatehistorytypesGettableRuleStateWindow: + properties: + end: + format: int64 + type: integer + start: + format: int64 + type: integer + state: + $ref: '#/components/schemas/RulestatehistorytypesAlertState' + required: + - state + - start + - end + type: object ServiceaccounttypesFactorAPIKey: properties: createdAt: @@ -7923,6 +8057,518 @@ paths: summary: Get users by role id tags: - users + /api/v2/rules/{id}/history/filter_keys: + get: + deprecated: false + description: Returns distinct label keys from rule history entries for the selected + range. + operationId: GetRuleHistoryFilterKeys + parameters: + - in: query + name: signal + schema: + $ref: '#/components/schemas/TelemetrytypesSignal' + - in: query + name: source + schema: + $ref: '#/components/schemas/TelemetrytypesSource' + - in: query + name: limit + schema: + type: integer + - in: query + name: startUnixMilli + schema: + format: int64 + type: integer + - in: query + name: endUnixMilli + schema: + format: int64 + type: integer + - in: query + name: fieldContext + schema: + $ref: '#/components/schemas/TelemetrytypesFieldContext' + - in: query + name: fieldDataType + schema: + $ref: '#/components/schemas/TelemetrytypesFieldDataType' + - in: query + name: metricName + schema: + type: string + - in: query + name: searchText + schema: + type: string + - in: path + name: id + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + properties: + data: + $ref: '#/components/schemas/TelemetrytypesGettableFieldKeys' + status: + type: string + required: + - status + - data + type: object + description: OK + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Bad Request + "401": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Unauthorized + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Forbidden + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Internal Server Error + security: + - api_key: + - VIEWER + - tokenizer: + - VIEWER + summary: Get rule history filter keys + tags: + - rules + /api/v2/rules/{id}/history/filter_values: + get: + deprecated: false + description: Returns distinct label values for a given key from rule history + entries. + operationId: GetRuleHistoryFilterValues + parameters: + - in: query + name: signal + schema: + $ref: '#/components/schemas/TelemetrytypesSignal' + - in: query + name: source + schema: + $ref: '#/components/schemas/TelemetrytypesSource' + - in: query + name: limit + schema: + type: integer + - in: query + name: startUnixMilli + schema: + format: int64 + type: integer + - in: query + name: endUnixMilli + schema: + format: int64 + type: integer + - in: query + name: fieldContext + schema: + $ref: '#/components/schemas/TelemetrytypesFieldContext' + - in: query + name: fieldDataType + schema: + $ref: '#/components/schemas/TelemetrytypesFieldDataType' + - in: query + name: metricName + schema: + type: string + - in: query + name: searchText + schema: + type: string + - in: query + name: name + schema: + type: string + - in: query + name: existingQuery + schema: + type: string + - in: path + name: id + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + properties: + data: + $ref: '#/components/schemas/TelemetrytypesGettableFieldValues' + status: + type: string + required: + - status + - data + type: object + description: OK + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Bad Request + "401": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Unauthorized + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Forbidden + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Internal Server Error + security: + - api_key: + - VIEWER + - tokenizer: + - VIEWER + summary: Get rule history filter values + tags: + - rules + /api/v2/rules/{id}/history/overall_status: + get: + deprecated: false + description: Returns overall firing/inactive intervals for a rule in the selected + time range. + operationId: GetRuleHistoryOverallStatus + parameters: + - in: query + name: start + required: true + schema: + format: int64 + type: integer + - in: query + name: end + required: true + schema: + format: int64 + type: integer + - in: path + name: id + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + properties: + data: + items: + $ref: '#/components/schemas/RulestatehistorytypesGettableRuleStateWindow' + nullable: true + type: array + status: + type: string + required: + - status + - data + type: object + description: OK + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Bad Request + "401": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Unauthorized + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Forbidden + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Internal Server Error + security: + - api_key: + - VIEWER + - tokenizer: + - VIEWER + summary: Get rule overall status timeline + tags: + - rules + /api/v2/rules/{id}/history/stats: + get: + deprecated: false + description: Returns trigger and resolution statistics for a rule in the selected + time range. + operationId: GetRuleHistoryStats + parameters: + - in: query + name: start + required: true + schema: + format: int64 + type: integer + - in: query + name: end + required: true + schema: + format: int64 + type: integer + - in: path + name: id + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + properties: + data: + $ref: '#/components/schemas/RulestatehistorytypesGettableRuleStateHistoryStats' + status: + type: string + required: + - status + - data + type: object + description: OK + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Bad Request + "401": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Unauthorized + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Forbidden + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Internal Server Error + security: + - api_key: + - VIEWER + - tokenizer: + - VIEWER + summary: Get rule history stats + tags: + - rules + /api/v2/rules/{id}/history/timeline: + get: + deprecated: false + description: Returns paginated timeline entries for rule state transitions. + operationId: GetRuleHistoryTimeline + parameters: + - in: query + name: start + required: true + schema: + format: int64 + type: integer + - in: query + name: end + required: true + schema: + format: int64 + type: integer + - in: query + name: state + schema: + $ref: '#/components/schemas/RulestatehistorytypesAlertState' + - in: query + name: filterExpression + schema: + type: string + - in: query + name: limit + schema: + format: int64 + type: integer + - in: query + name: order + schema: + $ref: '#/components/schemas/Querybuildertypesv5OrderDirection' + - in: query + name: cursor + schema: + type: string + - in: path + name: id + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + properties: + data: + $ref: '#/components/schemas/RulestatehistorytypesGettableRuleStateTimeline' + status: + type: string + required: + - status + - data + type: object + description: OK + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Bad Request + "401": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Unauthorized + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Forbidden + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Internal Server Error + security: + - api_key: + - VIEWER + - tokenizer: + - VIEWER + summary: Get rule history timeline + tags: + - rules + /api/v2/rules/{id}/history/top_contributors: + get: + deprecated: false + description: Returns top label combinations contributing to rule firing in the + selected time range. + operationId: GetRuleHistoryTopContributors + parameters: + - in: query + name: start + required: true + schema: + format: int64 + type: integer + - in: query + name: end + required: true + schema: + format: int64 + type: integer + - in: path + name: id + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + properties: + data: + items: + $ref: '#/components/schemas/RulestatehistorytypesGettableRuleStateHistoryContributor' + nullable: true + type: array + status: + type: string + required: + - status + - data + type: object + description: OK + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Bad Request + "401": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Unauthorized + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Forbidden + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Internal Server Error + security: + - api_key: + - VIEWER + - tokenizer: + - VIEWER + summary: Get top contributors to rule firing + tags: + - rules /api/v2/sessions: delete: deprecated: false diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index 1b0ee3a0768..5aa530bdcc7 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -29,6 +29,7 @@ import ( "github.com/SigNoz/signoz/pkg/cache" "github.com/SigNoz/signoz/pkg/http/middleware" "github.com/SigNoz/signoz/pkg/modules/organization" + "github.com/SigNoz/signoz/pkg/modules/rulestatehistory" "github.com/SigNoz/signoz/pkg/prometheus" "github.com/SigNoz/signoz/pkg/querier" "github.com/SigNoz/signoz/pkg/signoz" @@ -106,6 +107,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) { signoz.TelemetryMetadataStore, signoz.Prometheus, signoz.Modules.OrgGetter, + signoz.Modules.RuleStateHistory, signoz.Querier, signoz.Instrumentation.ToProviderSettings(), signoz.QueryParser, @@ -344,28 +346,29 @@ func (s *Server) Stop(ctx context.Context) error { return nil } -func makeRulesManager(ch baseint.Reader, cache cache.Cache, alertmanager alertmanager.Alertmanager, sqlstore sqlstore.SQLStore, telemetryStore telemetrystore.TelemetryStore, metadataStore telemetrytypes.MetadataStore, prometheus prometheus.Prometheus, orgGetter organization.Getter, querier querier.Querier, providerSettings factory.ProviderSettings, queryParser queryparser.QueryParser) (*baserules.Manager, error) { +func makeRulesManager(ch baseint.Reader, cache cache.Cache, alertmanager alertmanager.Alertmanager, sqlstore sqlstore.SQLStore, telemetryStore telemetrystore.TelemetryStore, metadataStore telemetrytypes.MetadataStore, prometheus prometheus.Prometheus, orgGetter organization.Getter, ruleStateHistoryModule rulestatehistory.Module, querier querier.Querier, providerSettings factory.ProviderSettings, queryParser queryparser.QueryParser) (*baserules.Manager, error) { ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings) maintenanceStore := sqlrulestore.NewMaintenanceStore(sqlstore) // create manager opts managerOpts := &baserules.ManagerOptions{ - TelemetryStore: telemetryStore, - MetadataStore: metadataStore, - Prometheus: prometheus, - Context: context.Background(), - Reader: ch, - Querier: querier, - Logger: providerSettings.Logger, - Cache: cache, - EvalDelay: baseconst.GetEvalDelay(), - PrepareTaskFunc: rules.PrepareTaskFunc, - PrepareTestRuleFunc: rules.TestNotification, - Alertmanager: alertmanager, - OrgGetter: orgGetter, - RuleStore: ruleStore, - MaintenanceStore: maintenanceStore, - SqlStore: sqlstore, - QueryParser: queryParser, + TelemetryStore: telemetryStore, + MetadataStore: metadataStore, + Prometheus: prometheus, + Context: context.Background(), + Reader: ch, + Querier: querier, + Logger: providerSettings.Logger, + Cache: cache, + EvalDelay: baseconst.GetEvalDelay(), + PrepareTaskFunc: rules.PrepareTaskFunc, + PrepareTestRuleFunc: rules.TestNotification, + Alertmanager: alertmanager, + OrgGetter: orgGetter, + RuleStore: ruleStore, + MaintenanceStore: maintenanceStore, + SqlStore: sqlstore, + QueryParser: queryParser, + RuleStateHistoryModule: ruleStateHistoryModule, } // create Manager diff --git a/ee/query-service/rules/manager.go b/ee/query-service/rules/manager.go index a06e1ec6095..7c3c5b72a0f 100644 --- a/ee/query-service/rules/manager.go +++ b/ee/query-service/rules/manager.go @@ -28,6 +28,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error) if err != nil { return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "evaluation is invalid: %v", err) } + if opts.Rule.RuleType == ruletypes.RuleTypeThreshold { // create a threshold rule tr, err := baserules.NewThresholdRule( @@ -41,6 +42,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error) baserules.WithSQLStore(opts.SQLStore), baserules.WithQueryParser(opts.ManagerOpts.QueryParser), baserules.WithMetadataStore(opts.ManagerOpts.MetadataStore), + baserules.WithRuleStateHistoryModule(opts.ManagerOpts.RuleStateHistoryModule), ) if err != nil { @@ -65,6 +67,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error) baserules.WithSQLStore(opts.SQLStore), baserules.WithQueryParser(opts.ManagerOpts.QueryParser), baserules.WithMetadataStore(opts.ManagerOpts.MetadataStore), + baserules.WithRuleStateHistoryModule(opts.ManagerOpts.RuleStateHistoryModule), ) if err != nil { @@ -90,6 +93,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error) baserules.WithSQLStore(opts.SQLStore), baserules.WithQueryParser(opts.ManagerOpts.QueryParser), baserules.WithMetadataStore(opts.ManagerOpts.MetadataStore), + baserules.WithRuleStateHistoryModule(opts.ManagerOpts.RuleStateHistoryModule), ) if err != nil { return task, err diff --git a/frontend/src/api/generated/services/rules/index.ts b/frontend/src/api/generated/services/rules/index.ts new file mode 100644 index 00000000000..24097e84d2d --- /dev/null +++ b/frontend/src/api/generated/services/rules/index.ts @@ -0,0 +1,744 @@ +/** + * ! Do not edit manually + * * The file has been auto-generated using Orval for SigNoz + * * regenerate with 'yarn generate:api' + * SigNoz + */ +import type { + InvalidateOptions, + QueryClient, + QueryFunction, + QueryKey, + UseQueryOptions, + UseQueryResult, +} from 'react-query'; +import { useQuery } from 'react-query'; + +import type { ErrorType } from '../../../generatedAPIInstance'; +import { GeneratedAPIInstance } from '../../../generatedAPIInstance'; +import type { + GetRuleHistoryFilterKeys200, + GetRuleHistoryFilterKeysParams, + GetRuleHistoryFilterKeysPathParameters, + GetRuleHistoryFilterValues200, + GetRuleHistoryFilterValuesParams, + GetRuleHistoryFilterValuesPathParameters, + GetRuleHistoryOverallStatus200, + GetRuleHistoryOverallStatusParams, + GetRuleHistoryOverallStatusPathParameters, + GetRuleHistoryStats200, + GetRuleHistoryStatsParams, + GetRuleHistoryStatsPathParameters, + GetRuleHistoryTimeline200, + GetRuleHistoryTimelineParams, + GetRuleHistoryTimelinePathParameters, + GetRuleHistoryTopContributors200, + GetRuleHistoryTopContributorsParams, + GetRuleHistoryTopContributorsPathParameters, + RenderErrorResponseDTO, +} from '../sigNoz.schemas'; + +/** + * Returns distinct label keys from rule history entries for the selected range. + * @summary Get rule history filter keys + */ +export const getRuleHistoryFilterKeys = ( + { id }: GetRuleHistoryFilterKeysPathParameters, + params?: GetRuleHistoryFilterKeysParams, + signal?: AbortSignal, +) => { + return GeneratedAPIInstance({ + url: `/api/v2/rules/${id}/history/filter_keys`, + method: 'GET', + params, + signal, + }); +}; + +export const getGetRuleHistoryFilterKeysQueryKey = ( + { id }: GetRuleHistoryFilterKeysPathParameters, + params?: GetRuleHistoryFilterKeysParams, +) => { + return [ + `/api/v2/rules/${id}/history/filter_keys`, + ...(params ? [params] : []), + ] as const; +}; + +export const getGetRuleHistoryFilterKeysQueryOptions = < + TData = Awaited>, + TError = ErrorType +>( + { id }: GetRuleHistoryFilterKeysPathParameters, + params?: GetRuleHistoryFilterKeysParams, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + }, +) => { + const { query: queryOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? getGetRuleHistoryFilterKeysQueryKey({ id }, params); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => getRuleHistoryFilterKeys({ id }, params, signal); + + return { + queryKey, + queryFn, + enabled: !!id, + ...queryOptions, + } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: QueryKey }; +}; + +export type GetRuleHistoryFilterKeysQueryResult = NonNullable< + Awaited> +>; +export type GetRuleHistoryFilterKeysQueryError = ErrorType; + +/** + * @summary Get rule history filter keys + */ + +export function useGetRuleHistoryFilterKeys< + TData = Awaited>, + TError = ErrorType +>( + { id }: GetRuleHistoryFilterKeysPathParameters, + params?: GetRuleHistoryFilterKeysParams, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + }, +): UseQueryResult & { queryKey: QueryKey } { + const queryOptions = getGetRuleHistoryFilterKeysQueryOptions( + { id }, + params, + options, + ); + + const query = useQuery(queryOptions) as UseQueryResult & { + queryKey: QueryKey; + }; + + query.queryKey = queryOptions.queryKey; + + return query; +} + +/** + * @summary Get rule history filter keys + */ +export const invalidateGetRuleHistoryFilterKeys = async ( + queryClient: QueryClient, + { id }: GetRuleHistoryFilterKeysPathParameters, + params?: GetRuleHistoryFilterKeysParams, + options?: InvalidateOptions, +): Promise => { + await queryClient.invalidateQueries( + { queryKey: getGetRuleHistoryFilterKeysQueryKey({ id }, params) }, + options, + ); + + return queryClient; +}; + +/** + * Returns distinct label values for a given key from rule history entries. + * @summary Get rule history filter values + */ +export const getRuleHistoryFilterValues = ( + { id }: GetRuleHistoryFilterValuesPathParameters, + params?: GetRuleHistoryFilterValuesParams, + signal?: AbortSignal, +) => { + return GeneratedAPIInstance({ + url: `/api/v2/rules/${id}/history/filter_values`, + method: 'GET', + params, + signal, + }); +}; + +export const getGetRuleHistoryFilterValuesQueryKey = ( + { id }: GetRuleHistoryFilterValuesPathParameters, + params?: GetRuleHistoryFilterValuesParams, +) => { + return [ + `/api/v2/rules/${id}/history/filter_values`, + ...(params ? [params] : []), + ] as const; +}; + +export const getGetRuleHistoryFilterValuesQueryOptions = < + TData = Awaited>, + TError = ErrorType +>( + { id }: GetRuleHistoryFilterValuesPathParameters, + params?: GetRuleHistoryFilterValuesParams, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + }, +) => { + const { query: queryOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? + getGetRuleHistoryFilterValuesQueryKey({ id }, params); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => getRuleHistoryFilterValues({ id }, params, signal); + + return { + queryKey, + queryFn, + enabled: !!id, + ...queryOptions, + } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: QueryKey }; +}; + +export type GetRuleHistoryFilterValuesQueryResult = NonNullable< + Awaited> +>; +export type GetRuleHistoryFilterValuesQueryError = ErrorType; + +/** + * @summary Get rule history filter values + */ + +export function useGetRuleHistoryFilterValues< + TData = Awaited>, + TError = ErrorType +>( + { id }: GetRuleHistoryFilterValuesPathParameters, + params?: GetRuleHistoryFilterValuesParams, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + }, +): UseQueryResult & { queryKey: QueryKey } { + const queryOptions = getGetRuleHistoryFilterValuesQueryOptions( + { id }, + params, + options, + ); + + const query = useQuery(queryOptions) as UseQueryResult & { + queryKey: QueryKey; + }; + + query.queryKey = queryOptions.queryKey; + + return query; +} + +/** + * @summary Get rule history filter values + */ +export const invalidateGetRuleHistoryFilterValues = async ( + queryClient: QueryClient, + { id }: GetRuleHistoryFilterValuesPathParameters, + params?: GetRuleHistoryFilterValuesParams, + options?: InvalidateOptions, +): Promise => { + await queryClient.invalidateQueries( + { queryKey: getGetRuleHistoryFilterValuesQueryKey({ id }, params) }, + options, + ); + + return queryClient; +}; + +/** + * Returns overall firing/inactive intervals for a rule in the selected time range. + * @summary Get rule overall status timeline + */ +export const getRuleHistoryOverallStatus = ( + { id }: GetRuleHistoryOverallStatusPathParameters, + params: GetRuleHistoryOverallStatusParams, + signal?: AbortSignal, +) => { + return GeneratedAPIInstance({ + url: `/api/v2/rules/${id}/history/overall_status`, + method: 'GET', + params, + signal, + }); +}; + +export const getGetRuleHistoryOverallStatusQueryKey = ( + { id }: GetRuleHistoryOverallStatusPathParameters, + params?: GetRuleHistoryOverallStatusParams, +) => { + return [ + `/api/v2/rules/${id}/history/overall_status`, + ...(params ? [params] : []), + ] as const; +}; + +export const getGetRuleHistoryOverallStatusQueryOptions = < + TData = Awaited>, + TError = ErrorType +>( + { id }: GetRuleHistoryOverallStatusPathParameters, + params: GetRuleHistoryOverallStatusParams, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + }, +) => { + const { query: queryOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? + getGetRuleHistoryOverallStatusQueryKey({ id }, params); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => getRuleHistoryOverallStatus({ id }, params, signal); + + return { + queryKey, + queryFn, + enabled: !!id, + ...queryOptions, + } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: QueryKey }; +}; + +export type GetRuleHistoryOverallStatusQueryResult = NonNullable< + Awaited> +>; +export type GetRuleHistoryOverallStatusQueryError = ErrorType; + +/** + * @summary Get rule overall status timeline + */ + +export function useGetRuleHistoryOverallStatus< + TData = Awaited>, + TError = ErrorType +>( + { id }: GetRuleHistoryOverallStatusPathParameters, + params: GetRuleHistoryOverallStatusParams, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + }, +): UseQueryResult & { queryKey: QueryKey } { + const queryOptions = getGetRuleHistoryOverallStatusQueryOptions( + { id }, + params, + options, + ); + + const query = useQuery(queryOptions) as UseQueryResult & { + queryKey: QueryKey; + }; + + query.queryKey = queryOptions.queryKey; + + return query; +} + +/** + * @summary Get rule overall status timeline + */ +export const invalidateGetRuleHistoryOverallStatus = async ( + queryClient: QueryClient, + { id }: GetRuleHistoryOverallStatusPathParameters, + params: GetRuleHistoryOverallStatusParams, + options?: InvalidateOptions, +): Promise => { + await queryClient.invalidateQueries( + { queryKey: getGetRuleHistoryOverallStatusQueryKey({ id }, params) }, + options, + ); + + return queryClient; +}; + +/** + * Returns trigger and resolution statistics for a rule in the selected time range. + * @summary Get rule history stats + */ +export const getRuleHistoryStats = ( + { id }: GetRuleHistoryStatsPathParameters, + params: GetRuleHistoryStatsParams, + signal?: AbortSignal, +) => { + return GeneratedAPIInstance({ + url: `/api/v2/rules/${id}/history/stats`, + method: 'GET', + params, + signal, + }); +}; + +export const getGetRuleHistoryStatsQueryKey = ( + { id }: GetRuleHistoryStatsPathParameters, + params?: GetRuleHistoryStatsParams, +) => { + return [ + `/api/v2/rules/${id}/history/stats`, + ...(params ? [params] : []), + ] as const; +}; + +export const getGetRuleHistoryStatsQueryOptions = < + TData = Awaited>, + TError = ErrorType +>( + { id }: GetRuleHistoryStatsPathParameters, + params: GetRuleHistoryStatsParams, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + }, +) => { + const { query: queryOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? getGetRuleHistoryStatsQueryKey({ id }, params); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => getRuleHistoryStats({ id }, params, signal); + + return { + queryKey, + queryFn, + enabled: !!id, + ...queryOptions, + } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: QueryKey }; +}; + +export type GetRuleHistoryStatsQueryResult = NonNullable< + Awaited> +>; +export type GetRuleHistoryStatsQueryError = ErrorType; + +/** + * @summary Get rule history stats + */ + +export function useGetRuleHistoryStats< + TData = Awaited>, + TError = ErrorType +>( + { id }: GetRuleHistoryStatsPathParameters, + params: GetRuleHistoryStatsParams, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + }, +): UseQueryResult & { queryKey: QueryKey } { + const queryOptions = getGetRuleHistoryStatsQueryOptions( + { id }, + params, + options, + ); + + const query = useQuery(queryOptions) as UseQueryResult & { + queryKey: QueryKey; + }; + + query.queryKey = queryOptions.queryKey; + + return query; +} + +/** + * @summary Get rule history stats + */ +export const invalidateGetRuleHistoryStats = async ( + queryClient: QueryClient, + { id }: GetRuleHistoryStatsPathParameters, + params: GetRuleHistoryStatsParams, + options?: InvalidateOptions, +): Promise => { + await queryClient.invalidateQueries( + { queryKey: getGetRuleHistoryStatsQueryKey({ id }, params) }, + options, + ); + + return queryClient; +}; + +/** + * Returns paginated timeline entries for rule state transitions. + * @summary Get rule history timeline + */ +export const getRuleHistoryTimeline = ( + { id }: GetRuleHistoryTimelinePathParameters, + params: GetRuleHistoryTimelineParams, + signal?: AbortSignal, +) => { + return GeneratedAPIInstance({ + url: `/api/v2/rules/${id}/history/timeline`, + method: 'GET', + params, + signal, + }); +}; + +export const getGetRuleHistoryTimelineQueryKey = ( + { id }: GetRuleHistoryTimelinePathParameters, + params?: GetRuleHistoryTimelineParams, +) => { + return [ + `/api/v2/rules/${id}/history/timeline`, + ...(params ? [params] : []), + ] as const; +}; + +export const getGetRuleHistoryTimelineQueryOptions = < + TData = Awaited>, + TError = ErrorType +>( + { id }: GetRuleHistoryTimelinePathParameters, + params: GetRuleHistoryTimelineParams, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + }, +) => { + const { query: queryOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? getGetRuleHistoryTimelineQueryKey({ id }, params); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => getRuleHistoryTimeline({ id }, params, signal); + + return { + queryKey, + queryFn, + enabled: !!id, + ...queryOptions, + } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: QueryKey }; +}; + +export type GetRuleHistoryTimelineQueryResult = NonNullable< + Awaited> +>; +export type GetRuleHistoryTimelineQueryError = ErrorType; + +/** + * @summary Get rule history timeline + */ + +export function useGetRuleHistoryTimeline< + TData = Awaited>, + TError = ErrorType +>( + { id }: GetRuleHistoryTimelinePathParameters, + params: GetRuleHistoryTimelineParams, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + }, +): UseQueryResult & { queryKey: QueryKey } { + const queryOptions = getGetRuleHistoryTimelineQueryOptions( + { id }, + params, + options, + ); + + const query = useQuery(queryOptions) as UseQueryResult & { + queryKey: QueryKey; + }; + + query.queryKey = queryOptions.queryKey; + + return query; +} + +/** + * @summary Get rule history timeline + */ +export const invalidateGetRuleHistoryTimeline = async ( + queryClient: QueryClient, + { id }: GetRuleHistoryTimelinePathParameters, + params: GetRuleHistoryTimelineParams, + options?: InvalidateOptions, +): Promise => { + await queryClient.invalidateQueries( + { queryKey: getGetRuleHistoryTimelineQueryKey({ id }, params) }, + options, + ); + + return queryClient; +}; + +/** + * Returns top label combinations contributing to rule firing in the selected time range. + * @summary Get top contributors to rule firing + */ +export const getRuleHistoryTopContributors = ( + { id }: GetRuleHistoryTopContributorsPathParameters, + params: GetRuleHistoryTopContributorsParams, + signal?: AbortSignal, +) => { + return GeneratedAPIInstance({ + url: `/api/v2/rules/${id}/history/top_contributors`, + method: 'GET', + params, + signal, + }); +}; + +export const getGetRuleHistoryTopContributorsQueryKey = ( + { id }: GetRuleHistoryTopContributorsPathParameters, + params?: GetRuleHistoryTopContributorsParams, +) => { + return [ + `/api/v2/rules/${id}/history/top_contributors`, + ...(params ? [params] : []), + ] as const; +}; + +export const getGetRuleHistoryTopContributorsQueryOptions = < + TData = Awaited>, + TError = ErrorType +>( + { id }: GetRuleHistoryTopContributorsPathParameters, + params: GetRuleHistoryTopContributorsParams, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + }, +) => { + const { query: queryOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? + getGetRuleHistoryTopContributorsQueryKey({ id }, params); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => getRuleHistoryTopContributors({ id }, params, signal); + + return { + queryKey, + queryFn, + enabled: !!id, + ...queryOptions, + } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: QueryKey }; +}; + +export type GetRuleHistoryTopContributorsQueryResult = NonNullable< + Awaited> +>; +export type GetRuleHistoryTopContributorsQueryError = ErrorType; + +/** + * @summary Get top contributors to rule firing + */ + +export function useGetRuleHistoryTopContributors< + TData = Awaited>, + TError = ErrorType +>( + { id }: GetRuleHistoryTopContributorsPathParameters, + params: GetRuleHistoryTopContributorsParams, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + }, +): UseQueryResult & { queryKey: QueryKey } { + const queryOptions = getGetRuleHistoryTopContributorsQueryOptions( + { id }, + params, + options, + ); + + const query = useQuery(queryOptions) as UseQueryResult & { + queryKey: QueryKey; + }; + + query.queryKey = queryOptions.queryKey; + + return query; +} + +/** + * @summary Get top contributors to rule firing + */ +export const invalidateGetRuleHistoryTopContributors = async ( + queryClient: QueryClient, + { id }: GetRuleHistoryTopContributorsPathParameters, + params: GetRuleHistoryTopContributorsParams, + options?: InvalidateOptions, +): Promise => { + await queryClient.invalidateQueries( + { queryKey: getGetRuleHistoryTopContributorsQueryKey({ id }, params) }, + options, + ); + + return queryClient; +}; diff --git a/frontend/src/api/generated/services/sigNoz.schemas.ts b/frontend/src/api/generated/services/sigNoz.schemas.ts index 0f582592233..b904cdc4962 100644 --- a/frontend/src/api/generated/services/sigNoz.schemas.ts +++ b/frontend/src/api/generated/services/sigNoz.schemas.ts @@ -2677,6 +2677,139 @@ export interface RenderErrorResponseDTO { status: string; } +export enum RulestatehistorytypesAlertStateDTO { + inactive = 'inactive', + pending = 'pending', + recovering = 'recovering', + firing = 'firing', + nodata = 'nodata', + disabled = 'disabled', +} +export interface RulestatehistorytypesGettableRuleStateHistoryDTO { + /** + * @type integer + * @minimum 0 + */ + fingerprint: number; + /** + * @type array + * @nullable true + */ + labels: Querybuildertypesv5LabelDTO[] | null; + overallState: RulestatehistorytypesAlertStateDTO; + /** + * @type boolean + */ + overallStateChanged: boolean; + /** + * @type string + */ + ruleID: string; + /** + * @type string + */ + ruleName: string; + state: RulestatehistorytypesAlertStateDTO; + /** + * @type boolean + */ + stateChanged: boolean; + /** + * @type integer + * @format int64 + */ + unixMilli: number; + /** + * @type number + * @format double + */ + value: number; +} + +export interface RulestatehistorytypesGettableRuleStateHistoryContributorDTO { + /** + * @type integer + * @minimum 0 + */ + count: number; + /** + * @type integer + * @minimum 0 + */ + fingerprint: number; + /** + * @type array + * @nullable true + */ + labels: Querybuildertypesv5LabelDTO[] | null; + /** + * @type string + */ + relatedLogsLink?: string; + /** + * @type string + */ + relatedTracesLink?: string; +} + +export interface RulestatehistorytypesGettableRuleStateHistoryStatsDTO { + /** + * @type number + * @format double + */ + currentAvgResolutionTime: number; + currentAvgResolutionTimeSeries: Querybuildertypesv5TimeSeriesDTO; + currentTriggersSeries: Querybuildertypesv5TimeSeriesDTO; + /** + * @type number + * @format double + */ + pastAvgResolutionTime: number; + pastAvgResolutionTimeSeries: Querybuildertypesv5TimeSeriesDTO; + pastTriggersSeries: Querybuildertypesv5TimeSeriesDTO; + /** + * @type integer + * @minimum 0 + */ + totalCurrentTriggers: number; + /** + * @type integer + * @minimum 0 + */ + totalPastTriggers: number; +} + +export interface RulestatehistorytypesGettableRuleStateTimelineDTO { + /** + * @type array + * @nullable true + */ + items: RulestatehistorytypesGettableRuleStateHistoryDTO[] | null; + /** + * @type string + */ + nextCursor?: string; + /** + * @type integer + * @minimum 0 + */ + total: number; +} + +export interface RulestatehistorytypesGettableRuleStateWindowDTO { + /** + * @type integer + * @format int64 + */ + end: number; + /** + * @type integer + * @format int64 + */ + start: number; + state: RulestatehistorytypesAlertStateDTO; +} + export interface ServiceaccounttypesFactorAPIKeyDTO { /** * @type string @@ -4312,6 +4445,266 @@ export type GetUsersByRoleID200 = { status: string; }; +export type GetRuleHistoryFilterKeysPathParameters = { + id: string; +}; +export type GetRuleHistoryFilterKeysParams = { + /** + * @description undefined + */ + signal?: TelemetrytypesSignalDTO; + /** + * @description undefined + */ + source?: TelemetrytypesSourceDTO; + /** + * @type integer + * @description undefined + */ + limit?: number; + /** + * @type integer + * @format int64 + * @description undefined + */ + startUnixMilli?: number; + /** + * @type integer + * @format int64 + * @description undefined + */ + endUnixMilli?: number; + /** + * @description undefined + */ + fieldContext?: TelemetrytypesFieldContextDTO; + /** + * @description undefined + */ + fieldDataType?: TelemetrytypesFieldDataTypeDTO; + /** + * @type string + * @description undefined + */ + metricName?: string; + /** + * @type string + * @description undefined + */ + searchText?: string; +}; + +export type GetRuleHistoryFilterKeys200 = { + data: TelemetrytypesGettableFieldKeysDTO; + /** + * @type string + */ + status: string; +}; + +export type GetRuleHistoryFilterValuesPathParameters = { + id: string; +}; +export type GetRuleHistoryFilterValuesParams = { + /** + * @description undefined + */ + signal?: TelemetrytypesSignalDTO; + /** + * @description undefined + */ + source?: TelemetrytypesSourceDTO; + /** + * @type integer + * @description undefined + */ + limit?: number; + /** + * @type integer + * @format int64 + * @description undefined + */ + startUnixMilli?: number; + /** + * @type integer + * @format int64 + * @description undefined + */ + endUnixMilli?: number; + /** + * @description undefined + */ + fieldContext?: TelemetrytypesFieldContextDTO; + /** + * @description undefined + */ + fieldDataType?: TelemetrytypesFieldDataTypeDTO; + /** + * @type string + * @description undefined + */ + metricName?: string; + /** + * @type string + * @description undefined + */ + searchText?: string; + /** + * @type string + * @description undefined + */ + name?: string; + /** + * @type string + * @description undefined + */ + existingQuery?: string; +}; + +export type GetRuleHistoryFilterValues200 = { + data: TelemetrytypesGettableFieldValuesDTO; + /** + * @type string + */ + status: string; +}; + +export type GetRuleHistoryOverallStatusPathParameters = { + id: string; +}; +export type GetRuleHistoryOverallStatusParams = { + /** + * @type integer + * @format int64 + * @description undefined + */ + start: number; + /** + * @type integer + * @format int64 + * @description undefined + */ + end: number; +}; + +export type GetRuleHistoryOverallStatus200 = { + /** + * @type array + * @nullable true + */ + data: RulestatehistorytypesGettableRuleStateWindowDTO[] | null; + /** + * @type string + */ + status: string; +}; + +export type GetRuleHistoryStatsPathParameters = { + id: string; +}; +export type GetRuleHistoryStatsParams = { + /** + * @type integer + * @format int64 + * @description undefined + */ + start: number; + /** + * @type integer + * @format int64 + * @description undefined + */ + end: number; +}; + +export type GetRuleHistoryStats200 = { + data: RulestatehistorytypesGettableRuleStateHistoryStatsDTO; + /** + * @type string + */ + status: string; +}; + +export type GetRuleHistoryTimelinePathParameters = { + id: string; +}; +export type GetRuleHistoryTimelineParams = { + /** + * @type integer + * @format int64 + * @description undefined + */ + start: number; + /** + * @type integer + * @format int64 + * @description undefined + */ + end: number; + /** + * @description undefined + */ + state?: RulestatehistorytypesAlertStateDTO; + /** + * @type string + * @description undefined + */ + filterExpression?: string; + /** + * @type integer + * @format int64 + * @description undefined + */ + limit?: number; + /** + * @description undefined + */ + order?: Querybuildertypesv5OrderDirectionDTO; + /** + * @type string + * @description undefined + */ + cursor?: string; +}; + +export type GetRuleHistoryTimeline200 = { + data: RulestatehistorytypesGettableRuleStateTimelineDTO; + /** + * @type string + */ + status: string; +}; + +export type GetRuleHistoryTopContributorsPathParameters = { + id: string; +}; +export type GetRuleHistoryTopContributorsParams = { + /** + * @type integer + * @format int64 + * @description undefined + */ + start: number; + /** + * @type integer + * @format int64 + * @description undefined + */ + end: number; +}; + +export type GetRuleHistoryTopContributors200 = { + /** + * @type array + * @nullable true + */ + data: RulestatehistorytypesGettableRuleStateHistoryContributorDTO[] | null; + /** + * @type string + */ + status: string; +}; + export type GetSessionContext200 = { data: AuthtypesSessionContextDTO; /** diff --git a/pkg/apiserver/signozapiserver/provider.go b/pkg/apiserver/signozapiserver/provider.go index 1beeb8ccd2f..c4d5616653e 100644 --- a/pkg/apiserver/signozapiserver/provider.go +++ b/pkg/apiserver/signozapiserver/provider.go @@ -20,6 +20,7 @@ import ( "github.com/SigNoz/signoz/pkg/modules/preference" "github.com/SigNoz/signoz/pkg/modules/promote" "github.com/SigNoz/signoz/pkg/modules/rawdataexport" + "github.com/SigNoz/signoz/pkg/modules/rulestatehistory" "github.com/SigNoz/signoz/pkg/modules/serviceaccount" "github.com/SigNoz/signoz/pkg/modules/session" "github.com/SigNoz/signoz/pkg/modules/user" @@ -55,6 +56,7 @@ type provider struct { serviceAccountHandler serviceaccount.Handler factoryHandler factory.Handler cloudIntegrationHandler cloudintegration.Handler + ruleStateHistoryHandler rulestatehistory.Handler } func NewFactory( @@ -80,6 +82,7 @@ func NewFactory( serviceAccountHandler serviceaccount.Handler, factoryHandler factory.Handler, cloudIntegrationHandler cloudintegration.Handler, + ruleStateHistoryHandler rulestatehistory.Handler, ) factory.ProviderFactory[apiserver.APIServer, apiserver.Config] { return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, providerSettings factory.ProviderSettings, config apiserver.Config) (apiserver.APIServer, error) { return newProvider( @@ -108,6 +111,7 @@ func NewFactory( serviceAccountHandler, factoryHandler, cloudIntegrationHandler, + ruleStateHistoryHandler, ) }) } @@ -138,6 +142,7 @@ func newProvider( serviceAccountHandler serviceaccount.Handler, factoryHandler factory.Handler, cloudIntegrationHandler cloudintegration.Handler, + ruleStateHistoryHandler rulestatehistory.Handler, ) (apiserver.APIServer, error) { settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/apiserver/signozapiserver") router := mux.NewRouter().UseEncodedPath() @@ -166,6 +171,7 @@ func newProvider( serviceAccountHandler: serviceAccountHandler, factoryHandler: factoryHandler, cloudIntegrationHandler: cloudIntegrationHandler, + ruleStateHistoryHandler: ruleStateHistoryHandler, } provider.authZ = middleware.NewAuthZ(settings.Logger(), orgGetter, authz) @@ -262,6 +268,10 @@ func (provider *provider) AddToRouter(router *mux.Router) error { return err } + if err := provider.addRuleStateHistoryRoutes(router); err != nil { + return err + } + return nil } diff --git a/pkg/apiserver/signozapiserver/rulestatehistory.go b/pkg/apiserver/signozapiserver/rulestatehistory.go new file mode 100644 index 00000000000..931ab33aae8 --- /dev/null +++ b/pkg/apiserver/signozapiserver/rulestatehistory.go @@ -0,0 +1,118 @@ +package signozapiserver + +import ( + "net/http" + + "github.com/SigNoz/signoz/pkg/http/handler" + "github.com/SigNoz/signoz/pkg/types" + "github.com/SigNoz/signoz/pkg/types/rulestatehistorytypes" + "github.com/SigNoz/signoz/pkg/types/telemetrytypes" + "github.com/gorilla/mux" +) + +func (provider *provider) addRuleStateHistoryRoutes(router *mux.Router) error { + + if err := router.Handle("/api/v2/rules/{id}/history/stats", handler.New( + provider.authZ.ViewAccess(provider.ruleStateHistoryHandler.GetRuleHistoryStats), + handler.OpenAPIDef{ + ID: "GetRuleHistoryStats", + Tags: []string{"rules"}, + Summary: "Get rule history stats", + Description: "Returns trigger and resolution statistics for a rule in the selected time range.", + RequestQuery: new(rulestatehistorytypes.PostableRuleStateHistoryBaseQuery), + Response: new(rulestatehistorytypes.GettableRuleStateHistoryStats), + ResponseContentType: "application/json", + SuccessStatusCode: http.StatusOK, + ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError}, + SecuritySchemes: newSecuritySchemes(types.RoleViewer), + })).Methods(http.MethodGet).GetError(); err != nil { + return err + } + + if err := router.Handle("/api/v2/rules/{id}/history/timeline", handler.New( + provider.authZ.ViewAccess(provider.ruleStateHistoryHandler.GetRuleHistoryTimeline), + handler.OpenAPIDef{ + ID: "GetRuleHistoryTimeline", + Tags: []string{"rules"}, + Summary: "Get rule history timeline", + Description: "Returns paginated timeline entries for rule state transitions.", + RequestQuery: new(rulestatehistorytypes.PostableRuleStateHistoryTimelineQuery), + Response: new(rulestatehistorytypes.GettableRuleStateTimeline), + ResponseContentType: "application/json", + SuccessStatusCode: http.StatusOK, + ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError}, + SecuritySchemes: newSecuritySchemes(types.RoleViewer), + })).Methods(http.MethodGet).GetError(); err != nil { + return err + } + + if err := router.Handle("/api/v2/rules/{id}/history/top_contributors", handler.New( + provider.authZ.ViewAccess(provider.ruleStateHistoryHandler.GetRuleHistoryContributors), + handler.OpenAPIDef{ + ID: "GetRuleHistoryTopContributors", + Tags: []string{"rules"}, + Summary: "Get top contributors to rule firing", + Description: "Returns top label combinations contributing to rule firing in the selected time range.", + RequestQuery: new(rulestatehistorytypes.PostableRuleStateHistoryBaseQuery), + Response: new([]rulestatehistorytypes.GettableRuleStateHistoryContributor), + ResponseContentType: "application/json", + SuccessStatusCode: http.StatusOK, + ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError}, + SecuritySchemes: newSecuritySchemes(types.RoleViewer), + })).Methods(http.MethodGet).GetError(); err != nil { + return err + } + + if err := router.Handle("/api/v2/rules/{id}/history/filter_keys", handler.New( + provider.authZ.ViewAccess(provider.ruleStateHistoryHandler.GetRuleHistoryFilterKeys), + handler.OpenAPIDef{ + ID: "GetRuleHistoryFilterKeys", + Tags: []string{"rules"}, + Summary: "Get rule history filter keys", + Description: "Returns distinct label keys from rule history entries for the selected range.", + RequestQuery: new(telemetrytypes.PostableFieldKeysParams), + Response: new(telemetrytypes.GettableFieldKeys), + ResponseContentType: "application/json", + SuccessStatusCode: http.StatusOK, + ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError}, + SecuritySchemes: newSecuritySchemes(types.RoleViewer), + })).Methods(http.MethodGet).GetError(); err != nil { + return err + } + + if err := router.Handle("/api/v2/rules/{id}/history/filter_values", handler.New( + provider.authZ.ViewAccess(provider.ruleStateHistoryHandler.GetRuleHistoryFilterValues), + handler.OpenAPIDef{ + ID: "GetRuleHistoryFilterValues", + Tags: []string{"rules"}, + Summary: "Get rule history filter values", + Description: "Returns distinct label values for a given key from rule history entries.", + RequestQuery: new(telemetrytypes.PostableFieldValueParams), + Response: new(telemetrytypes.GettableFieldValues), + ResponseContentType: "application/json", + SuccessStatusCode: http.StatusOK, + ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError}, + SecuritySchemes: newSecuritySchemes(types.RoleViewer), + })).Methods(http.MethodGet).GetError(); err != nil { + return err + } + + if err := router.Handle("/api/v2/rules/{id}/history/overall_status", handler.New( + provider.authZ.ViewAccess(provider.ruleStateHistoryHandler.GetRuleHistoryOverallStatus), + handler.OpenAPIDef{ + ID: "GetRuleHistoryOverallStatus", + Tags: []string{"rules"}, + Summary: "Get rule overall status timeline", + Description: "Returns overall firing/inactive intervals for a rule in the selected time range.", + RequestQuery: new(rulestatehistorytypes.PostableRuleStateHistoryBaseQuery), + Response: new([]rulestatehistorytypes.GettableRuleStateWindow), + ResponseContentType: "application/json", + SuccessStatusCode: http.StatusOK, + ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError}, + SecuritySchemes: newSecuritySchemes(types.RoleViewer), + })).Methods(http.MethodGet).GetError(); err != nil { + return err + } + + return nil +} diff --git a/pkg/modules/rulestatehistory/implrulestatehistory/condition_builder.go b/pkg/modules/rulestatehistory/implrulestatehistory/condition_builder.go new file mode 100644 index 00000000000..23b53435d01 --- /dev/null +++ b/pkg/modules/rulestatehistory/implrulestatehistory/condition_builder.go @@ -0,0 +1,106 @@ +package implrulestatehistory + +import ( + "context" + "fmt" + "slices" + + "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/querybuilder" + qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" + "github.com/SigNoz/signoz/pkg/types/telemetrytypes" + "github.com/huandu/go-sqlbuilder" +) + +type conditionBuilder struct { + fm qbtypes.FieldMapper +} + +func newConditionBuilder(fm qbtypes.FieldMapper) qbtypes.ConditionBuilder { + return &conditionBuilder{fm: fm} +} + +func (c *conditionBuilder) ConditionFor( + ctx context.Context, + startNs uint64, + endNs uint64, + key *telemetrytypes.TelemetryFieldKey, + operator qbtypes.FilterOperator, + value any, + sb *sqlbuilder.SelectBuilder, +) (string, error) { + if operator.IsStringSearchOperator() { + value = querybuilder.FormatValueForContains(value) + } + + fieldName, err := c.fm.FieldFor(ctx, startNs, endNs, key) + if err != nil { + return "", err + } + + switch operator { + case qbtypes.FilterOperatorEqual: + return sb.E(fieldName, value), nil + case qbtypes.FilterOperatorNotEqual: + return sb.NE(fieldName, value), nil + case qbtypes.FilterOperatorGreaterThan: + return sb.G(fieldName, value), nil + case qbtypes.FilterOperatorGreaterThanOrEq: + return sb.GE(fieldName, value), nil + case qbtypes.FilterOperatorLessThan: + return sb.LT(fieldName, value), nil + case qbtypes.FilterOperatorLessThanOrEq: + return sb.LE(fieldName, value), nil + case qbtypes.FilterOperatorLike: + return sb.Like(fieldName, value), nil + case qbtypes.FilterOperatorNotLike: + return sb.NotLike(fieldName, value), nil + case qbtypes.FilterOperatorILike: + return sb.ILike(fieldName, value), nil + case qbtypes.FilterOperatorNotILike: + return sb.NotILike(fieldName, value), nil + case qbtypes.FilterOperatorContains: + return sb.ILike(fieldName, fmt.Sprintf("%%%s%%", value)), nil + case qbtypes.FilterOperatorNotContains: + return sb.NotILike(fieldName, fmt.Sprintf("%%%s%%", value)), nil + case qbtypes.FilterOperatorRegexp: + return fmt.Sprintf(`match(%s, %s)`, sqlbuilder.Escape(fieldName), sb.Var(value)), nil + case qbtypes.FilterOperatorNotRegexp: + return fmt.Sprintf(`NOT match(%s, %s)`, sqlbuilder.Escape(fieldName), sb.Var(value)), nil + case qbtypes.FilterOperatorBetween: + values, ok := value.([]any) + if !ok || len(values) != 2 { + return "", qbtypes.ErrBetweenValues + } + return sb.Between(fieldName, values[0], values[1]), nil + case qbtypes.FilterOperatorNotBetween: + values, ok := value.([]any) + if !ok || len(values) != 2 { + return "", qbtypes.ErrBetweenValues + } + return sb.NotBetween(fieldName, values[0], values[1]), nil + case qbtypes.FilterOperatorIn: + values, ok := value.([]any) + if !ok { + return "", qbtypes.ErrInValues + } + return sb.In(fieldName, values), nil + case qbtypes.FilterOperatorNotIn: + values, ok := value.([]any) + if !ok { + return "", qbtypes.ErrInValues + } + return sb.NotIn(fieldName, values), nil + case qbtypes.FilterOperatorExists, qbtypes.FilterOperatorNotExists: + intrinsic := []string{"rule_id", "rule_name", "overall_state", "overall_state_changed", "state", "state_changed", "unix_milli", "fingerprint", "value"} + if slices.Contains(intrinsic, key.Name) { + return "true", nil + } + if operator == qbtypes.FilterOperatorExists { + return fmt.Sprintf("has(JSONExtractKeys(labels), %s)", sb.Var(key.Name)), nil + } + return fmt.Sprintf("not has(JSONExtractKeys(labels), %s)", sb.Var(key.Name)), nil + } + + return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported operator: %v", operator) +} diff --git a/pkg/modules/rulestatehistory/implrulestatehistory/field_mapper.go b/pkg/modules/rulestatehistory/implrulestatehistory/field_mapper.go new file mode 100644 index 00000000000..596abf5b480 --- /dev/null +++ b/pkg/modules/rulestatehistory/implrulestatehistory/field_mapper.go @@ -0,0 +1,66 @@ +package implrulestatehistory + +import ( + "context" + "fmt" + "strings" + + schema "github.com/SigNoz/signoz-otel-collector/cmd/signozschemamigrator/schema_migrator" + qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" + "github.com/SigNoz/signoz/pkg/types/telemetrytypes" + "github.com/huandu/go-sqlbuilder" +) + +var ruleStateHistoryColumns = map[string]*schema.Column{ + "rule_id": {Name: "rule_id", Type: schema.ColumnTypeString}, + "rule_name": {Name: "rule_name", Type: schema.ColumnTypeString}, + "overall_state": {Name: "overall_state", Type: schema.ColumnTypeString}, + "overall_state_changed": {Name: "overall_state_changed", Type: schema.ColumnTypeBool}, + "state": {Name: "state", Type: schema.ColumnTypeString}, + "state_changed": {Name: "state_changed", Type: schema.ColumnTypeBool}, + "unix_milli": {Name: "unix_milli", Type: schema.ColumnTypeInt64}, + "labels": {Name: "labels", Type: schema.ColumnTypeString}, + "fingerprint": {Name: "fingerprint", Type: schema.ColumnTypeUInt64}, + "value": {Name: "value", Type: schema.ColumnTypeFloat64}, +} + +type fieldMapper struct{} + +func newFieldMapper() qbtypes.FieldMapper { + return &fieldMapper{} +} + +func (m *fieldMapper) getColumn(_ context.Context, key *telemetrytypes.TelemetryFieldKey) (*schema.Column, error) { //nolint:unparam + name := strings.TrimSpace(key.Name) + if col, ok := ruleStateHistoryColumns[name]; ok { + return col, nil + } + return ruleStateHistoryColumns["labels"], nil +} + +func (m *fieldMapper) FieldFor(ctx context.Context, _, _ uint64, key *telemetrytypes.TelemetryFieldKey) (string, error) { + col, err := m.getColumn(ctx, key) + if err != nil { + return "", err + } + if col.Name == "labels" && key.Name != "labels" { + return fmt.Sprintf("JSONExtractString(labels, '%s')", strings.ReplaceAll(key.Name, "'", "\\'")), nil + } + return col.Name, nil +} + +func (m *fieldMapper) ColumnFor(ctx context.Context, _, _ uint64, key *telemetrytypes.TelemetryFieldKey) ([]*schema.Column, error) { + col, err := m.getColumn(ctx, key) + if err != nil { + return nil, err + } + return []*schema.Column{col}, nil +} + +func (m *fieldMapper) ColumnExpressionFor(ctx context.Context, tsStart, tsEnd uint64, field *telemetrytypes.TelemetryFieldKey, _ map[string][]*telemetrytypes.TelemetryFieldKey) (string, error) { + colName, err := m.FieldFor(ctx, tsStart, tsEnd, field) + if err != nil { + return "", err + } + return fmt.Sprintf("%s AS `%s`", sqlbuilder.Escape(colName), field.Name), nil +} diff --git a/pkg/modules/rulestatehistory/implrulestatehistory/handler.go b/pkg/modules/rulestatehistory/implrulestatehistory/handler.go new file mode 100644 index 00000000000..9eac93df9f3 --- /dev/null +++ b/pkg/modules/rulestatehistory/implrulestatehistory/handler.go @@ -0,0 +1,321 @@ +package implrulestatehistory + +import ( + "encoding/base64" + "encoding/json" + "net/http" + "strings" + + "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/http/binding" + "github.com/SigNoz/signoz/pkg/http/render" + "github.com/SigNoz/signoz/pkg/modules/rulestatehistory" + qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" + "github.com/SigNoz/signoz/pkg/types/rulestatehistorytypes" + "github.com/SigNoz/signoz/pkg/types/telemetrytypes" + "github.com/gorilla/mux" +) + +type handler struct { + module rulestatehistory.Module +} + +type ruleHistoryRequest struct { + Query rulestatehistorytypes.Query + Cursor string +} + +type cursorToken struct { + Offset int64 `json:"offset"` + Limit int64 `json:"limit"` +} + +func NewHandler(module rulestatehistory.Module) rulestatehistory.Handler { + return &handler{module: module} +} + +func (h *handler) GetRuleHistoryStats(w http.ResponseWriter, r *http.Request) { + ruleID := mux.Vars(r)["id"] + query, ok := h.parseV2BaseQueryRequest(w, r) + if !ok { + return + } + + stats, err := h.module.GetHistoryStats(r.Context(), ruleID, query) + if err != nil { + render.Error(w, err) + return + } + render.Success(w, http.StatusOK, stats) +} + +func (h *handler) GetRuleHistoryOverallStatus(w http.ResponseWriter, r *http.Request) { + ruleID := mux.Vars(r)["id"] + query, ok := h.parseV2BaseQueryRequest(w, r) + if !ok { + return + } + + res, err := h.module.GetHistoryOverallStatus(r.Context(), ruleID, query) + if err != nil { + render.Error(w, err) + return + } + render.Success(w, http.StatusOK, res) +} + +func (h *handler) GetRuleHistoryTimeline(w http.ResponseWriter, r *http.Request) { + ruleID := mux.Vars(r)["id"] + req, ok := h.parseV2TimelineQueryRequest(w, r) + if !ok { + return + } + if req.Cursor != "" { + token, err := decodeCursor(req.Cursor) + if err != nil { + render.Error(w, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "invalid cursor")) + return + } + req.Query.Offset = token.Offset + if req.Query.Limit == 0 { + req.Query.Limit = token.Limit + } + } + if req.Query.Limit == 0 { + req.Query.Limit = 50 + } + + timelineItems, timelineTotal, err := h.module.GetHistoryTimeline(r.Context(), ruleID, req.Query) + if err != nil { + render.Error(w, err) + return + } + + resp := rulestatehistorytypes.GettableRuleStateTimeline{} + resp.Items = make([]rulestatehistorytypes.GettableRuleStateHistory, 0, len(timelineItems)) + for _, item := range timelineItems { + resp.Items = append(resp.Items, rulestatehistorytypes.GettableRuleStateHistory{ + RuleID: item.RuleID, + RuleName: item.RuleName, + OverallState: item.OverallState, + OverallStateChanged: item.OverallStateChanged, + State: item.State, + StateChanged: item.StateChanged, + UnixMilli: item.UnixMilli, + Labels: item.Labels.ToQBLabels(), + Fingerprint: item.Fingerprint, + Value: item.Value, + }) + } + resp.Total = timelineTotal + if req.Query.Limit > 0 && req.Query.Offset+int64(len(timelineItems)) < int64(timelineTotal) { + nextOffset := req.Query.Offset + int64(len(timelineItems)) + nextCursor, err := encodeCursor(cursorToken{Offset: nextOffset, Limit: req.Query.Limit}) + if err != nil { + render.Error(w, err) + return + } + resp.NextCursor = nextCursor + } + render.Success(w, http.StatusOK, resp) +} + +func (h *handler) GetRuleHistoryContributors(w http.ResponseWriter, r *http.Request) { + ruleID := mux.Vars(r)["id"] + query, ok := h.parseV2BaseQueryRequest(w, r) + if !ok { + return + } + + res, err := h.module.GetHistoryContributors(r.Context(), ruleID, query) + if err != nil { + render.Error(w, err) + return + } + converted := make([]rulestatehistorytypes.GettableRuleStateHistoryContributor, 0, len(res)) + for _, item := range res { + converted = append(converted, rulestatehistorytypes.GettableRuleStateHistoryContributor{ + Fingerprint: item.Fingerprint, + Labels: item.Labels.ToQBLabels(), + Count: item.Count, + RelatedTracesLink: item.RelatedTracesLink, + RelatedLogsLink: item.RelatedLogsLink, + }) + } + render.Success(w, http.StatusOK, converted) +} + +func (h *handler) GetRuleHistoryFilterKeys(w http.ResponseWriter, r *http.Request) { + ruleID := mux.Vars(r)["id"] + query, search, limit, ok := h.parseV2FilterKeysRequest(w, r) + if !ok { + return + } + + res, err := h.module.GetHistoryFilterKeys(r.Context(), ruleID, query, search, limit) + if err != nil { + render.Error(w, err) + return + } + render.Success(w, http.StatusOK, res) +} + +func (h *handler) GetRuleHistoryFilterValues(w http.ResponseWriter, r *http.Request) { + ruleID := mux.Vars(r)["id"] + query, key, search, limit, ok := h.parseV2FilterValuesRequest(w, r) + if !ok { + return + } + + res, err := h.module.GetHistoryFilterValues(r.Context(), ruleID, key, query, search, limit) + if err != nil { + render.Error(w, err) + return + } + render.Success(w, http.StatusOK, res) +} + +func (h *handler) parseV2BaseQueryRequest(w http.ResponseWriter, r *http.Request) (rulestatehistorytypes.Query, bool) { + query, err := parseV2BaseQueryFromURL(r) + if err != nil { + render.Error(w, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "invalid query parameters")) + return rulestatehistorytypes.Query{}, false + } + if query.Start == 0 || query.End == 0 || query.Start >= query.End { + render.Error(w, errors.NewInvalidInputf(errors.CodeInvalidInput, "start and end are required and start must be less than end")) + return rulestatehistorytypes.Query{}, false + } + return query, true +} + +func (h *handler) parseV2TimelineQueryRequest(w http.ResponseWriter, r *http.Request) (*ruleHistoryRequest, bool) { + req, err := parseV2TimelineQueryFromURL(r) + if err != nil { + render.Error(w, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "invalid query parameters")) + return nil, false + } + if err := req.Query.Validate(); err != nil { + render.Error(w, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "invalid query parameters")) + return nil, false + } + return req, true +} + +func (h *handler) parseV2FilterKeysRequest(w http.ResponseWriter, r *http.Request) (rulestatehistorytypes.Query, string, int64, bool) { + raw := telemetrytypes.PostableFieldKeysParams{} + if err := binding.Query.BindQuery(r.URL.Query(), &raw); err != nil { + render.Error(w, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "invalid query parameters")) + return rulestatehistorytypes.Query{}, "", 0, false + } + + query := rulestatehistorytypes.Query{ + Start: raw.StartUnixMilli, + End: raw.EndUnixMilli, + FilterExpression: qbtypes.Filter{}, + Order: qbtypes.OrderDirectionAsc, + } + if err := query.Validate(); err != nil { + render.Error(w, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "invalid query parameters")) + return rulestatehistorytypes.Query{}, "", 0, false + } + + limit := normalizeFilterLimit(int64(raw.Limit)) + return query, strings.TrimSpace(raw.SearchText), limit, true +} + +func (h *handler) parseV2FilterValuesRequest(w http.ResponseWriter, r *http.Request) (rulestatehistorytypes.Query, string, string, int64, bool) { + raw := telemetrytypes.PostableFieldValueParams{} + if err := binding.Query.BindQuery(r.URL.Query(), &raw); err != nil { + render.Error(w, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "invalid query parameters")) + return rulestatehistorytypes.Query{}, "", "", 0, false + } + + key := strings.TrimSpace(raw.Name) + if key == "" { + render.Error(w, errors.NewInvalidInputf(errors.CodeInvalidInput, "key is required")) + return rulestatehistorytypes.Query{}, "", "", 0, false + } + + query := rulestatehistorytypes.Query{ + Start: raw.StartUnixMilli, + End: raw.EndUnixMilli, + FilterExpression: parseFilterExpression(raw.ExistingQuery), + Order: qbtypes.OrderDirectionAsc, + } + if err := query.Validate(); err != nil { + render.Error(w, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "invalid query parameters")) + return rulestatehistorytypes.Query{}, "", "", 0, false + } + + limit := normalizeFilterLimit(int64(raw.Limit)) + return query, key, strings.TrimSpace(raw.SearchText), limit, true +} + +func parseV2BaseQueryFromURL(r *http.Request) (rulestatehistorytypes.Query, error) { + raw := rulestatehistorytypes.PostableRuleStateHistoryBaseQuery{} + if err := binding.Query.BindQuery(r.URL.Query(), &raw); err != nil { + return rulestatehistorytypes.Query{}, err + } + + return rulestatehistorytypes.Query{ + Start: raw.Start, + End: raw.End, + }, nil +} + +func parseV2TimelineQueryFromURL(r *http.Request) (*ruleHistoryRequest, error) { + raw := rulestatehistorytypes.PostableRuleStateHistoryTimelineQuery{} + if err := binding.Query.BindQuery(r.URL.Query(), &raw); err != nil { + return nil, err + } + + req := &ruleHistoryRequest{} + req.Query.Start = raw.Start + req.Query.End = raw.End + req.Query.State = raw.State + req.Query.Limit = raw.Limit + req.Query.Order = raw.Order + req.Query.FilterExpression = parseFilterExpression(raw.FilterExpression) + req.Cursor = raw.Cursor + return req, nil +} + +func encodeCursor(token cursorToken) (string, error) { + data, err := json.Marshal(token) + if err != nil { + return "", err + } + return base64.RawURLEncoding.EncodeToString(data), nil +} + +func decodeCursor(cursor string) (*cursorToken, error) { + data, err := base64.RawURLEncoding.DecodeString(cursor) + if err != nil { + return nil, err + } + token := &cursorToken{} + if err := json.Unmarshal(data, token); err != nil { + return nil, err + } + return token, nil +} + +func normalizeFilterLimit(limit int64) int64 { + if limit <= 0 { + return 50 + } + if limit > 200 { + return 200 + } + return limit +} + +func parseFilterExpression(values ...string) qbtypes.Filter { + for _, value := range values { + expr := strings.TrimSpace(value) + if expr != "" { + return qbtypes.Filter{Expression: expr} + } + } + return qbtypes.Filter{} +} diff --git a/pkg/modules/rulestatehistory/implrulestatehistory/module.go b/pkg/modules/rulestatehistory/implrulestatehistory/module.go new file mode 100644 index 00000000000..56654222970 --- /dev/null +++ b/pkg/modules/rulestatehistory/implrulestatehistory/module.go @@ -0,0 +1,179 @@ +package implrulestatehistory + +import ( + "context" + "math" + "time" + + "github.com/SigNoz/signoz/pkg/modules/rulestatehistory" + "github.com/SigNoz/signoz/pkg/types/rulestatehistorytypes" + "github.com/SigNoz/signoz/pkg/types/telemetrytypes" +) + +type module struct { + store rulestatehistorytypes.Store +} + +func NewModule(store rulestatehistorytypes.Store) rulestatehistory.Module { + return &module{store: store} +} + +func (m *module) GetLastSavedRuleStateHistory(ctx context.Context, ruleID string) ([]rulestatehistorytypes.RuleStateHistory, error) { + return m.store.GetLastSavedRuleStateHistory(ctx, ruleID) +} + +func (m *module) GetHistoryTimeline(ctx context.Context, ruleID string, query rulestatehistorytypes.Query) ([]rulestatehistorytypes.RuleStateHistory, uint64, error) { + return m.store.ReadRuleStateHistoryByRuleID(ctx, ruleID, &query) +} + +func (m *module) GetHistoryFilterKeys(ctx context.Context, ruleID string, query rulestatehistorytypes.Query, search string, limit int64) (*telemetrytypes.GettableFieldKeys, error) { + return m.store.ReadRuleStateHistoryFilterKeysByRuleID(ctx, ruleID, &query, search, limit) +} + +func (m *module) GetHistoryFilterValues(ctx context.Context, ruleID string, key string, query rulestatehistorytypes.Query, search string, limit int64) (*telemetrytypes.GettableFieldValues, error) { + return m.store.ReadRuleStateHistoryFilterValuesByRuleID(ctx, ruleID, key, &query, search, limit) +} + +func (m *module) GetHistoryContributors(ctx context.Context, ruleID string, query rulestatehistorytypes.Query) ([]rulestatehistorytypes.RuleStateHistoryContributor, error) { + return m.store.ReadRuleStateHistoryTopContributorsByRuleID(ctx, ruleID, &query) +} + +func (m *module) GetHistoryOverallStatus(ctx context.Context, ruleID string, query rulestatehistorytypes.Query) ([]rulestatehistorytypes.GettableRuleStateWindow, error) { + return m.store.GetOverallStateTransitions(ctx, ruleID, &query) +} + +func (m *module) GetHistoryStats(ctx context.Context, ruleID string, params rulestatehistorytypes.Query) (rulestatehistorytypes.GettableRuleStateHistoryStats, error) { + totalCurrentTriggers, err := m.store.GetTotalTriggers(ctx, ruleID, ¶ms) + if err != nil { + return rulestatehistorytypes.GettableRuleStateHistoryStats{}, err + } + currentTriggersSeries, err := m.store.GetTriggersByInterval(ctx, ruleID, ¶ms) + if err != nil { + return rulestatehistorytypes.GettableRuleStateHistoryStats{}, err + } + currentAvgResolutionTime, err := m.store.GetAvgResolutionTime(ctx, ruleID, ¶ms) + if err != nil { + return rulestatehistorytypes.GettableRuleStateHistoryStats{}, err + } + currentAvgResolutionTimeSeries, err := m.store.GetAvgResolutionTimeByInterval(ctx, ruleID, ¶ms) + if err != nil { + return rulestatehistorytypes.GettableRuleStateHistoryStats{}, err + } + + if params.End-params.Start >= 86400000 { + days := int64(math.Ceil(float64(params.End-params.Start) / 86400000)) + params.Start -= days * 86400000 + params.End -= days * 86400000 + } else { + params.Start -= 86400000 + params.End -= 86400000 + } + + totalPastTriggers, err := m.store.GetTotalTriggers(ctx, ruleID, ¶ms) + if err != nil { + return rulestatehistorytypes.GettableRuleStateHistoryStats{}, err + } + pastTriggersSeries, err := m.store.GetTriggersByInterval(ctx, ruleID, ¶ms) + if err != nil { + return rulestatehistorytypes.GettableRuleStateHistoryStats{}, err + } + pastAvgResolutionTime, err := m.store.GetAvgResolutionTime(ctx, ruleID, ¶ms) + if err != nil { + return rulestatehistorytypes.GettableRuleStateHistoryStats{}, err + } + pastAvgResolutionTimeSeries, err := m.store.GetAvgResolutionTimeByInterval(ctx, ruleID, ¶ms) + if err != nil { + return rulestatehistorytypes.GettableRuleStateHistoryStats{}, err + } + + if math.IsNaN(currentAvgResolutionTime) || math.IsInf(currentAvgResolutionTime, 0) { + currentAvgResolutionTime = 0 + } + if math.IsNaN(pastAvgResolutionTime) || math.IsInf(pastAvgResolutionTime, 0) { + pastAvgResolutionTime = 0 + } + + return rulestatehistorytypes.GettableRuleStateHistoryStats{ + TotalCurrentTriggers: totalCurrentTriggers, + TotalPastTriggers: totalPastTriggers, + CurrentTriggersSeries: currentTriggersSeries, + PastTriggersSeries: pastTriggersSeries, + CurrentAvgResolutionTime: currentAvgResolutionTime, + PastAvgResolutionTime: pastAvgResolutionTime, + CurrentAvgResolutionTimeSeries: currentAvgResolutionTimeSeries, + PastAvgResolutionTimeSeries: pastAvgResolutionTimeSeries, + }, nil +} + +func (m *module) RecordRuleStateHistory(ctx context.Context, ruleID string, handledRestart bool, itemsToAdd []rulestatehistorytypes.RuleStateHistory) error { + revisedItemsToAdd := map[uint64]rulestatehistorytypes.RuleStateHistory{} + + lastSavedState, err := m.store.GetLastSavedRuleStateHistory(ctx, ruleID) + if err != nil { + return err + } + + if !handledRestart && len(lastSavedState) > 0 { + currentItemsByFingerprint := make(map[uint64]rulestatehistorytypes.RuleStateHistory, len(itemsToAdd)) + for _, item := range itemsToAdd { + currentItemsByFingerprint[item.Fingerprint] = item + } + + shouldSkip := map[uint64]bool{} + for _, item := range lastSavedState { + currentState, ok := currentItemsByFingerprint[item.Fingerprint] + if !ok { + if item.State == rulestatehistorytypes.StateFiring || item.State == rulestatehistorytypes.StateNoData { + item.State = rulestatehistorytypes.StateInactive + item.StateChanged = true + item.UnixMilli = time.Now().UnixMilli() + revisedItemsToAdd[item.Fingerprint] = item + } + } else if item.State != currentState.State { + item.State = currentState.State + item.StateChanged = true + item.UnixMilli = time.Now().UnixMilli() + revisedItemsToAdd[item.Fingerprint] = item + } + + shouldSkip[item.Fingerprint] = true + } + + for _, item := range itemsToAdd { + if _, ok := revisedItemsToAdd[item.Fingerprint]; !ok && !shouldSkip[item.Fingerprint] { + revisedItemsToAdd[item.Fingerprint] = item + } + } + + newState := rulestatehistorytypes.StateInactive + for _, item := range revisedItemsToAdd { + if item.State == rulestatehistorytypes.StateFiring || item.State == rulestatehistorytypes.StateNoData { + newState = rulestatehistorytypes.StateFiring + break + } + } + + if lastSavedState[0].OverallState != newState { + for fingerprint, item := range revisedItemsToAdd { + item.OverallState = newState + item.OverallStateChanged = true + revisedItemsToAdd[fingerprint] = item + } + } + } else { + for _, item := range itemsToAdd { + revisedItemsToAdd[item.Fingerprint] = item + } + } + + if len(revisedItemsToAdd) == 0 { + return nil + } + + entries := make([]rulestatehistorytypes.RuleStateHistory, 0, len(revisedItemsToAdd)) + for _, item := range revisedItemsToAdd { + entries = append(entries, item) + } + + return m.store.AddRuleStateHistory(ctx, entries) +} diff --git a/pkg/modules/rulestatehistory/implrulestatehistory/store.go b/pkg/modules/rulestatehistory/implrulestatehistory/store.go new file mode 100644 index 00000000000..39c4934df02 --- /dev/null +++ b/pkg/modules/rulestatehistory/implrulestatehistory/store.go @@ -0,0 +1,574 @@ +package implrulestatehistory + +import ( + "context" + "fmt" + "log/slog" + "strings" + "time" + + "github.com/SigNoz/signoz/pkg/querybuilder" + "github.com/SigNoz/signoz/pkg/telemetrystore" + qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" + "github.com/SigNoz/signoz/pkg/types/rulestatehistorytypes" + "github.com/SigNoz/signoz/pkg/types/telemetrytypes" + sqlbuilder "github.com/huandu/go-sqlbuilder" +) + +const ( + signozHistoryDBName = "signoz_analytics" + ruleStateHistoryTableName = "distributed_rule_state_history_v0" +) + +type store struct { + telemetryStore telemetrystore.TelemetryStore + telemetryMetadataStore telemetrytypes.MetadataStore + fieldMapper qbtypes.FieldMapper + conditionBuilder qbtypes.ConditionBuilder + logger *slog.Logger +} + +func NewStore(telemetryStore telemetrystore.TelemetryStore, telemetryMetadataStore telemetrytypes.MetadataStore, logger *slog.Logger) rulestatehistorytypes.Store { + fm := newFieldMapper() + return &store{ + telemetryStore: telemetryStore, + telemetryMetadataStore: telemetryMetadataStore, + fieldMapper: fm, + conditionBuilder: newConditionBuilder(fm), + logger: logger, + } +} + +func (s *store) AddRuleStateHistory(ctx context.Context, entries []rulestatehistorytypes.RuleStateHistory) error { + ib := sqlbuilder.NewInsertBuilder() + ib.InsertInto(historyTable()) + ib.Cols( + "rule_id", + "rule_name", + "overall_state", + "overall_state_changed", + "state", + "state_changed", + "unix_milli", + "labels", + "fingerprint", + "value", + ) + insertQuery, _ := ib.BuildWithFlavor(sqlbuilder.ClickHouse) + + statement, err := s.telemetryStore.ClickhouseDB().PrepareBatch( + ctx, + insertQuery, + ) + if err != nil { + return err + } + defer statement.Abort() //nolint:errcheck + + for _, history := range entries { + if err = statement.Append( + history.RuleID, + history.RuleName, + history.OverallState, + history.OverallStateChanged, + history.State, + history.StateChanged, + history.UnixMilli, + history.Labels, + history.Fingerprint, + history.Value, + ); err != nil { + return err + } + } + return statement.Send() +} + +func (s *store) GetLastSavedRuleStateHistory(ctx context.Context, ruleID string) ([]rulestatehistorytypes.RuleStateHistory, error) { + sb := sqlbuilder.NewSelectBuilder() + sb.Select("*") + sb.From(historyTable()) + sb.Where(sb.E("rule_id", ruleID)) + sb.Where(sb.E("state_changed", true)) + sb.OrderBy("unix_milli DESC") + sb.SQL("LIMIT 1 BY fingerprint") + + query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse) + history := make([]rulestatehistorytypes.RuleStateHistory, 0) + if err := s.telemetryStore.ClickhouseDB().Select(ctx, &history, query, args...); err != nil { + return nil, err + } + return history, nil +} + +func (s *store) ReadRuleStateHistoryByRuleID(ctx context.Context, ruleID string, query *rulestatehistorytypes.Query) ([]rulestatehistorytypes.RuleStateHistory, uint64, error) { + sb := sqlbuilder.NewSelectBuilder() + sb.Select( + "rule_id", + "rule_name", + "overall_state", + "overall_state_changed", + "state", + "state_changed", + "unix_milli", + "labels", + "fingerprint", + "value", + ) + sb.From(historyTable()) + s.applyBaseHistoryFilters(sb, ruleID, query) + + whereClause, err := s.buildFilterClause(ctx, query.FilterExpression, query.Start, query.End) + if err != nil { + return nil, 0, err + } + if whereClause != nil { + sb.AddWhereClause(sqlbuilder.CopyWhereClause(whereClause)) + } + + sb.OrderBy(fmt.Sprintf("unix_milli %s", strings.ToUpper(query.Order.StringValue()))) + sb.Limit(int(query.Limit)) + sb.Offset(int(query.Offset)) + + selectQuery, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse) + + history := []rulestatehistorytypes.RuleStateHistory{} + if err := s.telemetryStore.ClickhouseDB().Select(ctx, &history, selectQuery, args...); err != nil { + return nil, 0, err + } + + countSB := sqlbuilder.NewSelectBuilder() + countSB.Select("count(*)") + countSB.From(historyTable()) + s.applyBaseHistoryFilters(countSB, ruleID, query) + if whereClause != nil { + countSB.AddWhereClause(sqlbuilder.CopyWhereClause(whereClause)) + } + + var total uint64 + countQuery, countArgs := countSB.BuildWithFlavor(sqlbuilder.ClickHouse) + if err := s.telemetryStore.ClickhouseDB().QueryRow(ctx, countQuery, countArgs...).Scan(&total); err != nil { + return nil, 0, err + } + + return history, total, nil +} + +func (s *store) ReadRuleStateHistoryFilterKeysByRuleID(ctx context.Context, ruleID string, query *rulestatehistorytypes.Query, search string, limit int64) (*telemetrytypes.GettableFieldKeys, error) { + if limit <= 0 { + limit = 50 + } + + sb := sqlbuilder.NewSelectBuilder() + keyExpr := "arrayJoin(JSONExtractKeys(labels))" + sb.Select(fmt.Sprintf("DISTINCT %s AS key", keyExpr)) + sb.From(historyTable()) + s.applyBaseHistoryFilters(sb, ruleID, query) + sb.Where(fmt.Sprintf("%s != ''", keyExpr)) + + search = strings.TrimSpace(search) + if search != "" { + sb.Where(fmt.Sprintf("positionCaseInsensitiveUTF8(%s, %s) > 0", keyExpr, sb.Var(search))) + } + + whereClause, err := s.buildFilterClause(ctx, query.FilterExpression, query.Start, query.End) + if err != nil { + return nil, err + } + if whereClause != nil { + sb.AddWhereClause(sqlbuilder.CopyWhereClause(whereClause)) + } + + sb.OrderBy("key ASC") + sb.Limit(int(limit + 1)) + selectQuery, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse) + + rows, err := s.telemetryStore.ClickhouseDB().Query(ctx, selectQuery, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + keys := make([]string, 0, limit+1) + for rows.Next() { + var key string + if err := rows.Scan(&key); err != nil { + return nil, err + } + key = strings.TrimSpace(key) + if key != "" { + keys = append(keys, key) + } + } + if err := rows.Err(); err != nil { + return nil, err + } + + complete := true + if int64(len(keys)) > limit { + keys = keys[:int(limit)] + complete = false + } + + keysMap := make(map[string][]*telemetrytypes.TelemetryFieldKey, len(keys)) + for _, key := range keys { + fieldKey := &telemetrytypes.TelemetryFieldKey{ + Name: key, + FieldDataType: telemetrytypes.FieldDataTypeString, + } + keysMap[key] = []*telemetrytypes.TelemetryFieldKey{fieldKey} + } + + return &telemetrytypes.GettableFieldKeys{ + Keys: keysMap, + Complete: complete, + }, nil +} + +func (s *store) ReadRuleStateHistoryFilterValuesByRuleID(ctx context.Context, ruleID string, key string, query *rulestatehistorytypes.Query, search string, limit int64) (*telemetrytypes.GettableFieldValues, error) { + if limit <= 0 { + limit = 50 + } + + sb := sqlbuilder.NewSelectBuilder() + valExpr := fmt.Sprintf("JSONExtractString(labels, %s)", sb.Var(key)) + sb.Select(fmt.Sprintf("DISTINCT %s AS val", valExpr)) + sb.From(historyTable()) + s.applyBaseHistoryFilters(sb, ruleID, query) + sb.Where(fmt.Sprintf("JSONHas(labels, %s)", sb.Var(key))) + sb.Where(fmt.Sprintf("%s != ''", valExpr)) + + search = strings.TrimSpace(search) + if search != "" { + sb.Where(fmt.Sprintf("positionCaseInsensitiveUTF8(%s, %s) > 0", valExpr, sb.Var(search))) + } + + whereClause, err := s.buildFilterClause(ctx, query.FilterExpression, query.Start, query.End) + if err != nil { + return nil, err + } + if whereClause != nil { + sb.AddWhereClause(sqlbuilder.CopyWhereClause(whereClause)) + } + + sb.OrderBy("val ASC") + sb.Limit(int(limit + 1)) + selectQuery, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse) + + rows, err := s.telemetryStore.ClickhouseDB().Query(ctx, selectQuery, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + values := make([]string, 0, limit+1) + for rows.Next() { + var value string + if err := rows.Scan(&value); err != nil { + return nil, err + } + value = strings.TrimSpace(value) + if value != "" { + values = append(values, value) + } + } + if err := rows.Err(); err != nil { + return nil, err + } + + complete := true + if int64(len(values)) > limit { + values = values[:int(limit)] + complete = false + } + + return &telemetrytypes.GettableFieldValues{ + Values: &telemetrytypes.TelemetryFieldValues{ + StringValues: values, + }, + Complete: complete, + }, nil +} + +func (s *store) ReadRuleStateHistoryTopContributorsByRuleID(ctx context.Context, ruleID string, query *rulestatehistorytypes.Query) ([]rulestatehistorytypes.RuleStateHistoryContributor, error) { + sb := sqlbuilder.NewSelectBuilder() + sb.Select( + "fingerprint", + "argMax(labels, unix_milli) AS labels", + "count(*) AS count", + ) + sb.From(historyTable()) + sb.Where(sb.E("rule_id", ruleID)) + sb.Where(sb.E("state_changed", true)) + sb.Where(sb.E("state", rulestatehistorytypes.StateFiring.StringValue())) + sb.Where(sb.GE("unix_milli", query.Start)) + sb.Where(sb.LT("unix_milli", query.End)) + + whereClause, err := s.buildFilterClause(ctx, query.FilterExpression, query.Start, query.End) + if err != nil { + return nil, err + } + if whereClause != nil { + sb.AddWhereClause(sqlbuilder.CopyWhereClause(whereClause)) + } + + sb.GroupBy("fingerprint") + sb.Having("labels != '{}'") + sb.OrderBy("count DESC") + selectQuery, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse) + + contributors := []rulestatehistorytypes.RuleStateHistoryContributor{} + if err := s.telemetryStore.ClickhouseDB().Select(ctx, &contributors, selectQuery, args...); err != nil { + return nil, err + } + return contributors, nil +} + +func (s *store) GetOverallStateTransitions(ctx context.Context, ruleID string, query *rulestatehistorytypes.Query) ([]rulestatehistorytypes.GettableRuleStateWindow, error) { + innerSB := sqlbuilder.NewSelectBuilder() + + eventsSubquery := fmt.Sprintf( + `SELECT %s AS ts, if(count(*) = 0, %s, argMax(overall_state, unix_milli)) AS state +FROM %s +WHERE rule_id = %s + AND unix_milli <= %s +UNION ALL +SELECT unix_milli AS ts, anyLast(overall_state) AS state +FROM %s +WHERE rule_id = %s + AND overall_state_changed = true + AND unix_milli > %s + AND unix_milli < %s +GROUP BY unix_milli`, + innerSB.Var(query.Start), + innerSB.Var(rulestatehistorytypes.StateInactive.StringValue()), + historyTable(), + innerSB.Var(ruleID), + innerSB.Var(query.Start), + historyTable(), + innerSB.Var(ruleID), + innerSB.Var(query.Start), + innerSB.Var(query.End), + ) + + innerSB.Select( + "state", + "ts AS start", + fmt.Sprintf( + "ifNull(leadInFrame(toNullable(ts), 1) OVER (ORDER BY ts ASC ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING), %s) AS end", + innerSB.Var(query.End), + ), + ) + innerSB.From(fmt.Sprintf("(%s) AS events", eventsSubquery)) + innerSB.OrderBy("start ASC") + innerQuery, args := innerSB.BuildWithFlavor(sqlbuilder.ClickHouse) + + outerSB := sqlbuilder.NewSelectBuilder() + outerSB.Select("state", "start", "end") + outerSB.From(fmt.Sprintf("(%s) AS windows", innerQuery)) + outerSB.Where("start < end") + selectQuery, outerArgs := outerSB.BuildWithFlavor(sqlbuilder.ClickHouse) + args = append(args, outerArgs...) + + windows := []rulestatehistorytypes.GettableRuleStateWindow{} + if err := s.telemetryStore.ClickhouseDB().Select(ctx, &windows, selectQuery, args...); err != nil { + return nil, err + } + + return windows, nil +} + +func (s *store) GetAvgResolutionTime(ctx context.Context, ruleID string, query *rulestatehistorytypes.Query) (float64, error) { + cte := s.buildMatchedEventsCTE(ruleID, query) + sb := cte.Select("ifNull(toFloat64(avg(resolution_time - firing_time)) / 1000, 0) AS avg_resolution_time") + sb.From("matched_events") + selectQuery, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse) + + var avg float64 + if err := s.telemetryStore.ClickhouseDB().QueryRow(ctx, selectQuery, args...).Scan(&avg); err != nil { + return 0, err + } + return avg, nil +} + +func (s *store) GetAvgResolutionTimeByInterval(ctx context.Context, ruleID string, query *rulestatehistorytypes.Query) (*qbtypes.TimeSeries, error) { + step := minStepSeconds(query.Start, query.End) + cte := s.buildMatchedEventsCTE(ruleID, query) + sb := cte.Select( + fmt.Sprintf("toFloat64(avg(resolution_time - firing_time)) / 1000 AS value, toStartOfInterval(toDateTime(intDiv(firing_time, 1000)), INTERVAL %d SECOND) AS ts", step), + ) + sb.From("matched_events") + sb.GroupBy("ts") + sb.OrderBy("ts ASC") + selectQuery, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse) + + return s.querySeries(ctx, selectQuery, args...) +} + +func (s *store) GetTotalTriggers(ctx context.Context, ruleID string, query *rulestatehistorytypes.Query) (uint64, error) { + sb := sqlbuilder.NewSelectBuilder() + sb.Select("count(*)") + sb.From(historyTable()) + sb.Where(sb.E("rule_id", ruleID)) + sb.Where(sb.E("state_changed", true)) + sb.Where(sb.E("state", rulestatehistorytypes.StateFiring.StringValue())) + sb.Where(sb.GE("unix_milli", query.Start)) + sb.Where(sb.LT("unix_milli", query.End)) + selectQuery, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse) + + var total uint64 + if err := s.telemetryStore.ClickhouseDB().QueryRow(ctx, selectQuery, args...).Scan(&total); err != nil { + return 0, err + } + return total, nil +} + +func (s *store) GetTriggersByInterval(ctx context.Context, ruleID string, query *rulestatehistorytypes.Query) (*qbtypes.TimeSeries, error) { + step := minStepSeconds(query.Start, query.End) + sb := sqlbuilder.NewSelectBuilder() + sb.Select( + fmt.Sprintf("toFloat64(count(*)) AS value, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL %d SECOND) AS ts", step), + ) + sb.From(historyTable()) + sb.Where(sb.E("rule_id", ruleID)) + sb.Where(sb.E("state_changed", true)) + sb.Where(sb.E("state", rulestatehistorytypes.StateFiring.StringValue())) + sb.Where(sb.GE("unix_milli", query.Start)) + sb.Where(sb.LT("unix_milli", query.End)) + sb.GroupBy("ts") + sb.OrderBy("ts ASC") + selectQuery, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse) + + return s.querySeries(ctx, selectQuery, args...) +} + +func (s *store) querySeries(ctx context.Context, selectQuery string, args ...any) (*qbtypes.TimeSeries, error) { + rows, err := s.telemetryStore.ClickhouseDB().Query(ctx, selectQuery, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + series := &qbtypes.TimeSeries{ + Labels: []*qbtypes.Label{}, + Values: []*qbtypes.TimeSeriesValue{}, + } + + for rows.Next() { + var value float64 + var ts time.Time + if err := rows.Scan(&value, &ts); err != nil { + return nil, err + } + series.Values = append(series.Values, &qbtypes.TimeSeriesValue{ + Timestamp: ts.UnixMilli(), + Value: value, + }) + } + if err := rows.Err(); err != nil { + return nil, err + } + return series, nil +} + +func (s *store) buildFilterClause(ctx context.Context, filter qbtypes.Filter, startMillis, endMillis int64) (*sqlbuilder.WhereClause, error) { + expression := strings.TrimSpace(filter.Expression) + if expression == "" { + return nil, nil //nolint:nilnil + } + + selectors := querybuilder.QueryStringToKeysSelectors(expression) + for i := range selectors { + selectors[i].SelectorMatchType = telemetrytypes.FieldSelectorMatchTypeExact + } + + fieldKeys, _, err := s.telemetryMetadataStore.GetKeysMulti(ctx, selectors) + if err != nil || len(fieldKeys) == 0 { + fieldKeys = map[string][]*telemetrytypes.TelemetryFieldKey{} + for _, sel := range selectors { + fieldKeys[sel.Name] = []*telemetrytypes.TelemetryFieldKey{{ + Name: sel.Name, + Signal: sel.Signal, + FieldContext: sel.FieldContext, + FieldDataType: sel.FieldDataType, + }} + } + } + + opts := querybuilder.FilterExprVisitorOpts{ + Logger: s.logger, + FieldMapper: s.fieldMapper, + ConditionBuilder: s.conditionBuilder, + FieldKeys: fieldKeys, + FullTextColumn: &telemetrytypes.TelemetryFieldKey{Name: "labels", FieldContext: telemetrytypes.FieldContextAttribute}, + } + + opts.StartNs = querybuilder.ToNanoSecs(uint64(startMillis)) + opts.EndNs = querybuilder.ToNanoSecs(uint64(endMillis)) + prepared, err := querybuilder.PrepareWhereClause(expression, opts) + if err != nil { + return nil, err + } + if prepared == nil || prepared.WhereClause == nil { + return nil, nil //nolint:nilnil + } + return prepared.WhereClause, nil +} + +func (s *store) applyBaseHistoryFilters(sb *sqlbuilder.SelectBuilder, ruleID string, query *rulestatehistorytypes.Query) { + sb.Where(sb.E("rule_id", ruleID)) + sb.Where(sb.GE("unix_milli", query.Start)) + sb.Where(sb.LT("unix_milli", query.End)) + if !query.State.IsZero() { + sb.Where(sb.E("state", query.State.StringValue())) + } +} + +func (s *store) buildMatchedEventsCTE(ruleID string, query *rulestatehistorytypes.Query) *sqlbuilder.CTEBuilder { + firingSB := sqlbuilder.NewSelectBuilder() + firingSB.Select("rule_id", "unix_milli AS firing_time") + firingSB.From(historyTable()) + firingSB.Where(firingSB.E("overall_state", rulestatehistorytypes.StateFiring.StringValue())) + firingSB.Where(firingSB.E("overall_state_changed", true)) + firingSB.Where(firingSB.E("rule_id", ruleID)) + firingSB.Where(firingSB.GE("unix_milli", query.Start)) + firingSB.Where(firingSB.LT("unix_milli", query.End)) + + resolutionSB := sqlbuilder.NewSelectBuilder() + resolutionSB.Select("rule_id", "unix_milli AS resolution_time") + resolutionSB.From(historyTable()) + resolutionSB.Where(resolutionSB.E("overall_state", rulestatehistorytypes.StateInactive.StringValue())) + resolutionSB.Where(resolutionSB.E("overall_state_changed", true)) + resolutionSB.Where(resolutionSB.E("rule_id", ruleID)) + resolutionSB.Where(resolutionSB.GE("unix_milli", query.Start)) + resolutionSB.Where(resolutionSB.LT("unix_milli", query.End)) + + matchedSB := sqlbuilder.NewSelectBuilder() + matchedSB.Select("f.rule_id", "f.firing_time", "min(r.resolution_time) AS resolution_time") + matchedSB.From("firing_events f") + matchedSB.JoinWithOption(sqlbuilder.LeftJoin, "resolution_events r", "f.rule_id = r.rule_id") + matchedSB.Where("r.resolution_time > f.firing_time") + matchedSB.GroupBy("f.rule_id", "f.firing_time") + + return sqlbuilder.With( + sqlbuilder.CTEQuery("firing_events").As(firingSB), + sqlbuilder.CTEQuery("resolution_events").As(resolutionSB), + sqlbuilder.CTEQuery("matched_events").As(matchedSB), + ) +} + +func historyTable() string { + return fmt.Sprintf("%s.%s", signozHistoryDBName, ruleStateHistoryTableName) +} + +func minStepSeconds(start, end int64) int64 { + if end <= start { + return 60 + } + rangeSeconds := (end - start) / 1000 + if rangeSeconds <= 0 { + return 60 + } + step := rangeSeconds / 120 + return max(step, int64(60)) +} diff --git a/pkg/modules/rulestatehistory/rulestatehistory.go b/pkg/modules/rulestatehistory/rulestatehistory.go new file mode 100644 index 00000000000..b46bf2b0078 --- /dev/null +++ b/pkg/modules/rulestatehistory/rulestatehistory.go @@ -0,0 +1,61 @@ +package rulestatehistory + +import ( + "context" + "net/http" + + "github.com/SigNoz/signoz/pkg/types/rulestatehistorytypes" + "github.com/SigNoz/signoz/pkg/types/telemetrytypes" +) + +// Module defines the core operations for managing rule state history. +type Module interface { + // RecordRuleStateHistory persists a batch of rule state history entries for a given rule. + // The bool parameter indicates whether restart is handled. + // TODO(srikanthccv): remove when rule state history record moved to AM + RecordRuleStateHistory(context.Context, string, bool, []rulestatehistorytypes.RuleStateHistory) error + + // GetLastSavedRuleStateHistory retrieves the most recently saved state history entries for a given rule. + GetLastSavedRuleStateHistory(context.Context, string) ([]rulestatehistorytypes.RuleStateHistory, error) + + // GetHistoryStats returns aggregated statistics for rule state history matching the given query. + GetHistoryStats(context.Context, string, rulestatehistorytypes.Query) (rulestatehistorytypes.GettableRuleStateHistoryStats, error) + + // GetHistoryTimeline returns a time-ordered list of rule state history entries and a total count + // for the given query, suitable for paginated timeline views. + GetHistoryTimeline(context.Context, string, rulestatehistorytypes.Query) ([]rulestatehistorytypes.RuleStateHistory, uint64, error) + + // GetHistoryFilterKeys returns the available filter keys for rule state history queries. + GetHistoryFilterKeys(context.Context, string, rulestatehistorytypes.Query, string, int64) (*telemetrytypes.GettableFieldKeys, error) + + // GetHistoryFilterValues returns the available values for a specific filter key in rule state history. + GetHistoryFilterValues(context.Context, string, string, rulestatehistorytypes.Query, string, int64) (*telemetrytypes.GettableFieldValues, error) + + // GetHistoryContributors returns the top contributors to trigger alert, for the given query. + GetHistoryContributors(context.Context, string, rulestatehistorytypes.Query) ([]rulestatehistorytypes.RuleStateHistoryContributor, error) + + // GetHistoryOverallStatus returns the overall status windows for rule state history, + // providing an aggregated view of rule health over time. + GetHistoryOverallStatus(context.Context, string, rulestatehistorytypes.Query) ([]rulestatehistorytypes.GettableRuleStateWindow, error) +} + +// Handler defines the HTTP handler methods for rule state history API endpoints. +type Handler interface { + // GetRuleHistoryStats handles requests for aggregated rule state history statistics. + GetRuleHistoryStats(http.ResponseWriter, *http.Request) + + // GetRuleHistoryTimeline handles requests for a paginated timeline of rule state changes. + GetRuleHistoryTimeline(http.ResponseWriter, *http.Request) + + // GetRuleHistoryFilterKeys handles requests for available filter keys in rule state history. + GetRuleHistoryFilterKeys(http.ResponseWriter, *http.Request) + + // GetRuleHistoryFilterValues handles requests for available values of a specific filter key. + GetRuleHistoryFilterValues(http.ResponseWriter, *http.Request) + + // GetRuleHistoryContributors handles requests for top contributors to alert trigger. + GetRuleHistoryContributors(http.ResponseWriter, *http.Request) + + // GetRuleHistoryOverallStatus handles requests for the overall status view of rule health over time. + GetRuleHistoryOverallStatus(http.ResponseWriter, *http.Request) +} diff --git a/pkg/query-service/app/server.go b/pkg/query-service/app/server.go index adf3e6de826..c75cf94dc90 100644 --- a/pkg/query-service/app/server.go +++ b/pkg/query-service/app/server.go @@ -24,6 +24,7 @@ import ( "github.com/SigNoz/signoz/pkg/http/middleware" "github.com/SigNoz/signoz/pkg/licensing/nooplicensing" "github.com/SigNoz/signoz/pkg/modules/organization" + "github.com/SigNoz/signoz/pkg/modules/rulestatehistory" "github.com/SigNoz/signoz/pkg/prometheus" "github.com/SigNoz/signoz/pkg/querier" "github.com/SigNoz/signoz/pkg/query-service/agentConf" @@ -110,6 +111,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) { signoz.TelemetryMetadataStore, signoz.Prometheus, signoz.Modules.OrgGetter, + signoz.Modules.RuleStateHistory, signoz.Querier, signoz.Instrumentation.ToProviderSettings(), signoz.QueryParser, @@ -331,6 +333,7 @@ func makeRulesManager( metadataStore telemetrytypes.MetadataStore, prometheus prometheus.Prometheus, orgGetter organization.Getter, + ruleStateHistoryModule rulestatehistory.Module, querier querier.Querier, providerSettings factory.ProviderSettings, queryParser queryparser.QueryParser, @@ -339,21 +342,22 @@ func makeRulesManager( maintenanceStore := sqlrulestore.NewMaintenanceStore(sqlstore) // create manager opts managerOpts := &rules.ManagerOptions{ - TelemetryStore: telemetryStore, - MetadataStore: metadataStore, - Prometheus: prometheus, - Context: context.Background(), - Reader: ch, - Querier: querier, - Logger: providerSettings.Logger, - Cache: cache, - EvalDelay: constants.GetEvalDelay(), - OrgGetter: orgGetter, - Alertmanager: alertmanager, - RuleStore: ruleStore, - MaintenanceStore: maintenanceStore, - SqlStore: sqlstore, - QueryParser: queryParser, + TelemetryStore: telemetryStore, + MetadataStore: metadataStore, + Prometheus: prometheus, + Context: context.Background(), + Reader: ch, + Querier: querier, + Logger: providerSettings.Logger, + Cache: cache, + EvalDelay: constants.GetEvalDelay(), + OrgGetter: orgGetter, + Alertmanager: alertmanager, + RuleStore: ruleStore, + MaintenanceStore: maintenanceStore, + SqlStore: sqlstore, + QueryParser: queryParser, + RuleStateHistoryModule: ruleStateHistoryModule, } // create Manager diff --git a/pkg/query-service/rules/base_rule.go b/pkg/query-service/rules/base_rule.go index 0a20903d609..423f5c0673c 100644 --- a/pkg/query-service/rules/base_rule.go +++ b/pkg/query-service/rules/base_rule.go @@ -8,6 +8,7 @@ import ( "time" "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/modules/rulestatehistory" "github.com/SigNoz/signoz/pkg/query-service/constants" "github.com/SigNoz/signoz/pkg/query-service/interfaces" "github.com/SigNoz/signoz/pkg/query-service/model" @@ -17,6 +18,7 @@ import ( "github.com/SigNoz/signoz/pkg/queryparser" "github.com/SigNoz/signoz/pkg/sqlstore" qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" + "github.com/SigNoz/signoz/pkg/types/rulestatehistorytypes" "github.com/SigNoz/signoz/pkg/types/ruletypes" "github.com/SigNoz/signoz/pkg/types/telemetrytypes" "github.com/SigNoz/signoz/pkg/valuer" @@ -95,6 +97,8 @@ type BaseRule struct { // newGroupEvalDelay is the grace period for new alert groups newGroupEvalDelay valuer.TextDuration + ruleStateHistoryModule rulestatehistory.Module + queryParser queryparser.QueryParser } @@ -142,6 +146,12 @@ func WithMetadataStore(metadataStore telemetrytypes.MetadataStore) RuleOption { } } +func WithRuleStateHistoryModule(module rulestatehistory.Module) RuleOption { + return func(r *BaseRule) { + r.ruleStateHistoryModule = module + } +} + func NewBaseRule(id string, orgID valuer.UUID, p *ruletypes.PostableRule, reader interfaces.Reader, opts ...RuleOption) (*BaseRule, error) { if p.RuleCondition == nil || !p.RuleCondition.IsValid() { return nil, fmt.Errorf("invalid rule condition") @@ -399,100 +409,58 @@ func (r *BaseRule) ForEachActiveAlert(f func(*ruletypes.Alert)) { } func (r *BaseRule) RecordRuleStateHistory(ctx context.Context, prevState, currentState model.AlertState, itemsToAdd []model.RuleStateHistory) error { - r.logger.DebugContext(ctx, "recording rule state history", "ruleid", r.ID(), "prevState", prevState, "currentState", currentState, "itemsToAdd", itemsToAdd) - revisedItemsToAdd := map[uint64]model.RuleStateHistory{} - - lastSavedState, err := r.reader.GetLastSavedRuleStateHistory(ctx, r.ID()) - if err != nil { - return err - } - // if the query-service has been restarted, or the rule has been modified (which re-initializes the rule), - // the state would reset so we need to add the corresponding state changes to previously saved states - if !r.handledRestart && len(lastSavedState) > 0 { - r.logger.DebugContext(ctx, "handling restart", "ruleid", r.ID(), "lastSavedState", lastSavedState) - l := map[uint64]model.RuleStateHistory{} - for _, item := range itemsToAdd { - l[item.Fingerprint] = item - } - - shouldSkip := map[uint64]bool{} - - for _, item := range lastSavedState { - // for the last saved item with fingerprint, check if there is a corresponding entry in the current state - currentState, ok := l[item.Fingerprint] - if !ok { - // there was a state change in the past, but not in the current state - // if the state was firing, then we should add a resolved state change - if item.State == model.StateFiring || item.State == model.StateNoData { - item.State = model.StateInactive - item.StateChanged = true - item.UnixMilli = time.Now().UnixMilli() - revisedItemsToAdd[item.Fingerprint] = item - } - // there is nothing to do if the prev state was normal - } else { - if item.State != currentState.State { - item.State = currentState.State - item.StateChanged = true - item.UnixMilli = time.Now().UnixMilli() - revisedItemsToAdd[item.Fingerprint] = item - } - } - // do not add this item to revisedItemsToAdd as it is already processed - shouldSkip[item.Fingerprint] = true - } - r.logger.DebugContext(ctx, "after lastSavedState loop", "ruleid", r.ID(), "revisedItemsToAdd", revisedItemsToAdd) - - // if there are any new state changes that were not saved, add them to the revised items - for _, item := range itemsToAdd { - if _, ok := revisedItemsToAdd[item.Fingerprint]; !ok && !shouldSkip[item.Fingerprint] { - revisedItemsToAdd[item.Fingerprint] = item - } - } - r.logger.DebugContext(ctx, "after itemsToAdd loop", "ruleid", r.ID(), "revisedItemsToAdd", revisedItemsToAdd) - - newState := model.StateInactive - for _, item := range revisedItemsToAdd { - if item.State == model.StateFiring || item.State == model.StateNoData { - newState = model.StateFiring - break - } - } - r.logger.DebugContext(ctx, "newState", "ruleid", r.ID(), "newState", newState) - - // if there is a change in the overall state, update the overall state - if lastSavedState[0].OverallState != newState { - for fingerprint, item := range revisedItemsToAdd { - item.OverallState = newState - item.OverallStateChanged = true - revisedItemsToAdd[fingerprint] = item - } - } - r.logger.DebugContext(ctx, "revisedItemsToAdd after newState", "ruleid", r.ID(), "revisedItemsToAdd", revisedItemsToAdd) - - } else { - for _, item := range itemsToAdd { - revisedItemsToAdd[item.Fingerprint] = item - } + if r.ruleStateHistoryModule == nil { + return nil } - if len(revisedItemsToAdd) > 0 && r.reader != nil { - r.logger.DebugContext(ctx, "writing rule state history", "ruleid", r.ID(), "revisedItemsToAdd", revisedItemsToAdd) - - entries := make([]model.RuleStateHistory, 0, len(revisedItemsToAdd)) - for _, item := range revisedItemsToAdd { - entries = append(entries, item) - } - err := r.reader.AddRuleStateHistory(ctx, entries) - if err != nil { - r.logger.ErrorContext(ctx, "error while inserting rule state history", errors.Attr(err), "itemsToAdd", itemsToAdd) - } + if err := r.ruleStateHistoryModule.RecordRuleStateHistory(ctx, r.ID(), r.handledRestart, toRuleStateHistoryTypes(itemsToAdd)); err != nil { + r.logger.ErrorContext(ctx, "error while recording rule state history", errors.Attr(err), slog.Any("itemsToAdd", itemsToAdd)) + return err } r.handledRestart = true return nil } +// TODO(srikanthccv): remove these when v3 is cleaned up +func toRuleStateHistoryTypes(entries []model.RuleStateHistory) []rulestatehistorytypes.RuleStateHistory { + converted := make([]rulestatehistorytypes.RuleStateHistory, 0, len(entries)) + for _, entry := range entries { + converted = append(converted, rulestatehistorytypes.RuleStateHistory{ + RuleID: entry.RuleID, + RuleName: entry.RuleName, + OverallState: toRuleStateHistoryAlertState(entry.OverallState), + OverallStateChanged: entry.OverallStateChanged, + State: toRuleStateHistoryAlertState(entry.State), + StateChanged: entry.StateChanged, + UnixMilli: entry.UnixMilli, + Labels: rulestatehistorytypes.LabelsString(entry.Labels), + Fingerprint: entry.Fingerprint, + Value: entry.Value, + }) + } + return converted +} + +func toRuleStateHistoryAlertState(state model.AlertState) rulestatehistorytypes.AlertState { + switch state { + case model.StateInactive: + return rulestatehistorytypes.StateInactive + case model.StatePending: + return rulestatehistorytypes.StatePending + case model.StateRecovering: + return rulestatehistorytypes.StateRecovering + case model.StateFiring: + return rulestatehistorytypes.StateFiring + case model.StateNoData: + return rulestatehistorytypes.StateNoData + case model.StateDisabled: + return rulestatehistorytypes.StateDisabled + default: + return rulestatehistorytypes.StateInactive + } +} + func (r *BaseRule) PopulateTemporality(ctx context.Context, orgID valuer.UUID, qp *v3.QueryRangeParamsV3) error { missingTemporality := make([]string, 0) metricNameToTemporality := make(map[string]map[v3.Temporality]bool) diff --git a/pkg/query-service/rules/manager.go b/pkg/query-service/rules/manager.go index aa8735647c2..f727e6bc4c8 100644 --- a/pkg/query-service/rules/manager.go +++ b/pkg/query-service/rules/manager.go @@ -19,6 +19,7 @@ import ( "github.com/SigNoz/signoz/pkg/cache" "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/modules/organization" + "github.com/SigNoz/signoz/pkg/modules/rulestatehistory" "github.com/SigNoz/signoz/pkg/prometheus" querierV5 "github.com/SigNoz/signoz/pkg/querier" "github.com/SigNoz/signoz/pkg/query-service/interfaces" @@ -94,6 +95,8 @@ type ManagerOptions struct { EvalDelay valuer.TextDuration + RuleStateHistoryModule rulestatehistory.Module + PrepareTaskFunc func(opts PrepareTaskOptions) (Task, error) PrepareTestRuleFunc func(opts PrepareTestRuleOptions) (int, *model.ApiError) Alertmanager alertmanager.Alertmanager @@ -169,6 +172,7 @@ func defaultPrepareTaskFunc(opts PrepareTaskOptions) (Task, error) { WithSQLStore(opts.SQLStore), WithQueryParser(opts.ManagerOpts.QueryParser), WithMetadataStore(opts.ManagerOpts.MetadataStore), + WithRuleStateHistoryModule(opts.ManagerOpts.RuleStateHistoryModule), ) if err != nil { @@ -193,6 +197,7 @@ func defaultPrepareTaskFunc(opts PrepareTaskOptions) (Task, error) { WithSQLStore(opts.SQLStore), WithQueryParser(opts.ManagerOpts.QueryParser), WithMetadataStore(opts.ManagerOpts.MetadataStore), + WithRuleStateHistoryModule(opts.ManagerOpts.RuleStateHistoryModule), ) if err != nil { diff --git a/pkg/query-service/rules/threshold_rule_test.go b/pkg/query-service/rules/threshold_rule_test.go index a3c80768f1f..1f1fdc60ca9 100644 --- a/pkg/query-service/rules/threshold_rule_test.go +++ b/pkg/query-service/rules/threshold_rule_test.go @@ -36,7 +36,7 @@ func TestThresholdRuleEvalBackwardCompat(t *testing.T) { AlertName: "Eval Backward Compatibility Test without recovery target", AlertType: ruletypes.AlertTypeMetric, RuleType: ruletypes.RuleTypeThreshold, - Evaluation: &ruletypes.EvaluationEnvelope{ruletypes.RollingEvaluation, ruletypes.RollingWindow{ + Evaluation: &ruletypes.EvaluationEnvelope{Kind: ruletypes.RollingEvaluation, Spec: ruletypes.RollingWindow{ EvalWindow: valuer.MustParseTextDuration("5m"), Frequency: valuer.MustParseTextDuration("1m"), }}, @@ -152,7 +152,7 @@ func TestPrepareLinksToLogs(t *testing.T) { AlertName: "Tricky Condition Tests", AlertType: ruletypes.AlertTypeLogs, RuleType: ruletypes.RuleTypeThreshold, - Evaluation: &ruletypes.EvaluationEnvelope{ruletypes.RollingEvaluation, ruletypes.RollingWindow{ + Evaluation: &ruletypes.EvaluationEnvelope{Kind: ruletypes.RollingEvaluation, Spec: ruletypes.RollingWindow{ EvalWindow: valuer.MustParseTextDuration("5m"), Frequency: valuer.MustParseTextDuration("1m"), }}, @@ -206,7 +206,7 @@ func TestPrepareLinksToLogsV5(t *testing.T) { AlertName: "Tricky Condition Tests", AlertType: ruletypes.AlertTypeLogs, RuleType: ruletypes.RuleTypeThreshold, - Evaluation: &ruletypes.EvaluationEnvelope{ruletypes.RollingEvaluation, ruletypes.RollingWindow{ + Evaluation: &ruletypes.EvaluationEnvelope{Kind: ruletypes.RollingEvaluation, Spec: ruletypes.RollingWindow{ EvalWindow: valuer.MustParseTextDuration("5m"), Frequency: valuer.MustParseTextDuration("1m"), }}, @@ -267,7 +267,7 @@ func TestPrepareLinksToTracesV5(t *testing.T) { AlertName: "Tricky Condition Tests", AlertType: ruletypes.AlertTypeTraces, RuleType: ruletypes.RuleTypeThreshold, - Evaluation: &ruletypes.EvaluationEnvelope{ruletypes.RollingEvaluation, ruletypes.RollingWindow{ + Evaluation: &ruletypes.EvaluationEnvelope{Kind: ruletypes.RollingEvaluation, Spec: ruletypes.RollingWindow{ EvalWindow: valuer.MustParseTextDuration("5m"), Frequency: valuer.MustParseTextDuration("1m"), }}, @@ -328,7 +328,7 @@ func TestPrepareLinksToTraces(t *testing.T) { AlertName: "Links to traces test", AlertType: ruletypes.AlertTypeTraces, RuleType: ruletypes.RuleTypeThreshold, - Evaluation: &ruletypes.EvaluationEnvelope{ruletypes.RollingEvaluation, ruletypes.RollingWindow{ + Evaluation: &ruletypes.EvaluationEnvelope{Kind: ruletypes.RollingEvaluation, Spec: ruletypes.RollingWindow{ EvalWindow: valuer.MustParseTextDuration("5m"), Frequency: valuer.MustParseTextDuration("1m"), }}, @@ -382,7 +382,7 @@ func TestThresholdRuleLabelNormalization(t *testing.T) { AlertName: "Tricky Condition Tests", AlertType: ruletypes.AlertTypeMetric, RuleType: ruletypes.RuleTypeThreshold, - Evaluation: &ruletypes.EvaluationEnvelope{ruletypes.RollingEvaluation, ruletypes.RollingWindow{ + Evaluation: &ruletypes.EvaluationEnvelope{Kind: ruletypes.RollingEvaluation, Spec: ruletypes.RollingWindow{ EvalWindow: valuer.MustParseTextDuration("5m"), Frequency: valuer.MustParseTextDuration("1m"), }}, diff --git a/pkg/signoz/handler.go b/pkg/signoz/handler.go index 818d6e124c1..f80d50dc611 100644 --- a/pkg/signoz/handler.go +++ b/pkg/signoz/handler.go @@ -24,6 +24,8 @@ import ( "github.com/SigNoz/signoz/pkg/modules/quickfilter/implquickfilter" "github.com/SigNoz/signoz/pkg/modules/rawdataexport" "github.com/SigNoz/signoz/pkg/modules/rawdataexport/implrawdataexport" + "github.com/SigNoz/signoz/pkg/modules/rulestatehistory" + "github.com/SigNoz/signoz/pkg/modules/rulestatehistory/implrulestatehistory" "github.com/SigNoz/signoz/pkg/modules/savedview" "github.com/SigNoz/signoz/pkg/modules/savedview/implsavedview" "github.com/SigNoz/signoz/pkg/modules/serviceaccount" @@ -59,6 +61,7 @@ type Handlers struct { ServiceAccountHandler serviceaccount.Handler RegistryHandler factory.Handler CloudIntegrationHandler cloudintegration.Handler + RuleStateHistory rulestatehistory.Handler } func NewHandlers( @@ -95,5 +98,6 @@ func NewHandlers( ServiceAccountHandler: implserviceaccount.NewHandler(modules.ServiceAccount), RegistryHandler: registryHandler, CloudIntegrationHandler: implcloudintegration.NewHandler(), + RuleStateHistory: implrulestatehistory.NewHandler(modules.RuleStateHistory), } } diff --git a/pkg/signoz/module.go b/pkg/signoz/module.go index 7001143b92b..5245abf9927 100644 --- a/pkg/signoz/module.go +++ b/pkg/signoz/module.go @@ -25,6 +25,8 @@ import ( "github.com/SigNoz/signoz/pkg/modules/quickfilter/implquickfilter" "github.com/SigNoz/signoz/pkg/modules/rawdataexport" "github.com/SigNoz/signoz/pkg/modules/rawdataexport/implrawdataexport" + "github.com/SigNoz/signoz/pkg/modules/rulestatehistory" + "github.com/SigNoz/signoz/pkg/modules/rulestatehistory/implrulestatehistory" "github.com/SigNoz/signoz/pkg/modules/savedview" "github.com/SigNoz/signoz/pkg/modules/savedview/implsavedview" "github.com/SigNoz/signoz/pkg/modules/serviceaccount" @@ -51,24 +53,25 @@ import ( ) type Modules struct { - OrgGetter organization.Getter - OrgSetter organization.Setter - Preference preference.Module - UserSetter user.Setter - UserGetter user.Getter - SavedView savedview.Module - Apdex apdex.Module - Dashboard dashboard.Module - QuickFilter quickfilter.Module - TraceFunnel tracefunnel.Module - RawDataExport rawdataexport.Module - AuthDomain authdomain.Module - Session session.Module - Services services.Module - SpanPercentile spanpercentile.Module - MetricsExplorer metricsexplorer.Module - Promote promote.Module - ServiceAccount serviceaccount.Module + OrgGetter organization.Getter + OrgSetter organization.Setter + Preference preference.Module + UserSetter user.Setter + UserGetter user.Getter + SavedView savedview.Module + Apdex apdex.Module + Dashboard dashboard.Module + QuickFilter quickfilter.Module + TraceFunnel tracefunnel.Module + RawDataExport rawdataexport.Module + AuthDomain authdomain.Module + Session session.Module + Services services.Module + SpanPercentile spanpercentile.Module + MetricsExplorer metricsexplorer.Module + Promote promote.Module + ServiceAccount serviceaccount.Module + RuleStateHistory rulestatehistory.Module } func NewModules( @@ -97,23 +100,24 @@ func NewModules( ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings) return Modules{ - OrgGetter: orgGetter, - OrgSetter: orgSetter, - Preference: implpreference.NewModule(implpreference.NewStore(sqlstore), preferencetypes.NewAvailablePreference()), - SavedView: implsavedview.NewModule(sqlstore), - Apdex: implapdex.NewModule(sqlstore), - Dashboard: dashboard, - UserSetter: userSetter, - UserGetter: userGetter, - QuickFilter: quickfilter, - TraceFunnel: impltracefunnel.NewModule(impltracefunnel.NewStore(sqlstore)), - RawDataExport: implrawdataexport.NewModule(querier), - AuthDomain: implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs), - Session: implsession.NewModule(providerSettings, authNs, userSetter, userGetter, implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs), tokenizer, orgGetter), - SpanPercentile: implspanpercentile.NewModule(querier, providerSettings), - Services: implservices.NewModule(querier, telemetryStore), - MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, ruleStore, dashboard, providerSettings, config.MetricsExplorer), - Promote: implpromote.NewModule(telemetryMetadataStore, telemetryStore), - ServiceAccount: implserviceaccount.NewModule(implserviceaccount.NewStore(sqlstore), authz, emailing, providerSettings), + OrgGetter: orgGetter, + OrgSetter: orgSetter, + Preference: implpreference.NewModule(implpreference.NewStore(sqlstore), preferencetypes.NewAvailablePreference()), + SavedView: implsavedview.NewModule(sqlstore), + Apdex: implapdex.NewModule(sqlstore), + Dashboard: dashboard, + UserSetter: userSetter, + UserGetter: userGetter, + QuickFilter: quickfilter, + TraceFunnel: impltracefunnel.NewModule(impltracefunnel.NewStore(sqlstore)), + RawDataExport: implrawdataexport.NewModule(querier), + AuthDomain: implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs), + Session: implsession.NewModule(providerSettings, authNs, userSetter, userGetter, implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs), tokenizer, orgGetter), + SpanPercentile: implspanpercentile.NewModule(querier, providerSettings), + Services: implservices.NewModule(querier, telemetryStore), + MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, ruleStore, dashboard, providerSettings, config.MetricsExplorer), + Promote: implpromote.NewModule(telemetryMetadataStore, telemetryStore), + ServiceAccount: implserviceaccount.NewModule(implserviceaccount.NewStore(sqlstore), authz, emailing, providerSettings), + RuleStateHistory: implrulestatehistory.NewModule(implrulestatehistory.NewStore(telemetryStore, telemetryMetadataStore, providerSettings.Logger)), } } diff --git a/pkg/signoz/openapi.go b/pkg/signoz/openapi.go index cc6b8573964..51209a760da 100644 --- a/pkg/signoz/openapi.go +++ b/pkg/signoz/openapi.go @@ -25,6 +25,7 @@ import ( "github.com/SigNoz/signoz/pkg/modules/preference" "github.com/SigNoz/signoz/pkg/modules/promote" "github.com/SigNoz/signoz/pkg/modules/rawdataexport" + "github.com/SigNoz/signoz/pkg/modules/rulestatehistory" "github.com/SigNoz/signoz/pkg/modules/serviceaccount" "github.com/SigNoz/signoz/pkg/modules/session" "github.com/SigNoz/signoz/pkg/modules/user" @@ -67,6 +68,7 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta struct{ serviceaccount.Handler }{}, struct{ factory.Handler }{}, struct{ cloudintegration.Handler }{}, + struct{ rulestatehistory.Handler }{}, ).New(ctx, instrumentation.ToProviderSettings(), apiserver.Config{}) if err != nil { return nil, err diff --git a/pkg/signoz/provider.go b/pkg/signoz/provider.go index 83929159eb9..66f3f0266ec 100644 --- a/pkg/signoz/provider.go +++ b/pkg/signoz/provider.go @@ -280,6 +280,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au handlers.ServiceAccountHandler, handlers.RegistryHandler, handlers.CloudIntegrationHandler, + handlers.RuleStateHistory, ), ) } diff --git a/pkg/types/rulestatehistorytypes/http.go b/pkg/types/rulestatehistorytypes/http.go new file mode 100644 index 00000000000..27eb4277d97 --- /dev/null +++ b/pkg/types/rulestatehistorytypes/http.go @@ -0,0 +1,20 @@ +package rulestatehistorytypes + +import qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" + +// PostableRuleStateHistoryBaseQuery defines URL query params common across v2 rule history APIs. +type PostableRuleStateHistoryBaseQuery struct { + Start int64 `query:"start" required:"true"` + End int64 `query:"end" required:"true"` +} + +// PostableRuleStateHistoryTimelineQuery defines URL query params for timeline API. +type PostableRuleStateHistoryTimelineQuery struct { + Start int64 `query:"start" required:"true"` + End int64 `query:"end" required:"true"` + State AlertState `query:"state"` + FilterExpression string `query:"filterExpression"` + Limit int64 `query:"limit"` + Order qbtypes.OrderDirection `query:"order"` + Cursor string `query:"cursor"` +} diff --git a/pkg/types/rulestatehistorytypes/query.go b/pkg/types/rulestatehistorytypes/query.go new file mode 100644 index 00000000000..10c3625184c --- /dev/null +++ b/pkg/types/rulestatehistorytypes/query.go @@ -0,0 +1,35 @@ +package rulestatehistorytypes + +import ( + "github.com/SigNoz/signoz/pkg/errors" + qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" +) + +type Query struct { + Start int64 + End int64 + State AlertState + FilterExpression qbtypes.Filter + Limit int64 + Offset int64 + Order qbtypes.OrderDirection +} + +func (q *Query) Validate() error { + if q.Start == 0 || q.End == 0 { + return errors.NewInvalidInputf(errors.CodeInvalidInput, "start and end are required") + } + if q.Start >= q.End { + return errors.NewInvalidInputf(errors.CodeInvalidInput, "start must be less than end") + } + if q.Limit < 0 || q.Offset < 0 { + return errors.NewInvalidInputf(errors.CodeInvalidInput, "limit and offset must be greater than or equal to 0") + } + if q.Order.IsZero() { + q.Order = qbtypes.OrderDirectionDesc + } + if q.Order != qbtypes.OrderDirectionAsc && q.Order != qbtypes.OrderDirectionDesc { + return errors.NewInvalidInputf(errors.CodeInvalidInput, "order must be asc or desc") + } + return nil +} diff --git a/pkg/types/rulestatehistorytypes/response.go b/pkg/types/rulestatehistorytypes/response.go new file mode 100644 index 00000000000..bd17b3798e4 --- /dev/null +++ b/pkg/types/rulestatehistorytypes/response.go @@ -0,0 +1,49 @@ +package rulestatehistorytypes + +import ( + qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" +) + +type GettableRuleStateTimeline struct { + Items []GettableRuleStateHistory `json:"items" required:"true"` + Total uint64 `json:"total" required:"true"` + NextCursor string `json:"nextCursor,omitempty"` +} + +type GettableRuleStateHistory struct { + RuleID string `json:"ruleID" required:"true"` + RuleName string `json:"ruleName" required:"true"` + OverallState AlertState `json:"overallState" required:"true"` + OverallStateChanged bool `json:"overallStateChanged" required:"true"` + State AlertState `json:"state" required:"true"` + StateChanged bool `json:"stateChanged" required:"true"` + UnixMilli int64 `json:"unixMilli" required:"true"` + Labels []*qbtypes.Label `json:"labels" required:"true"` + Fingerprint uint64 `json:"fingerprint" required:"true"` + Value float64 `json:"value" required:"true"` +} + +type GettableRuleStateHistoryContributor struct { + Fingerprint uint64 `json:"fingerprint" required:"true"` + Labels []*qbtypes.Label `json:"labels" required:"true"` + Count uint64 `json:"count" required:"true"` + RelatedTracesLink string `json:"relatedTracesLink,omitempty"` + RelatedLogsLink string `json:"relatedLogsLink,omitempty"` +} + +type GettableRuleStateWindow struct { + State AlertState `json:"state" ch:"state" required:"true"` + Start int64 `json:"start" ch:"start" required:"true"` + End int64 `json:"end" ch:"end" required:"true"` +} + +type GettableRuleStateHistoryStats struct { + TotalCurrentTriggers uint64 `json:"totalCurrentTriggers" required:"true"` + TotalPastTriggers uint64 `json:"totalPastTriggers" required:"true"` + CurrentTriggersSeries *qbtypes.TimeSeries `json:"currentTriggersSeries" required:"true" nullable:"true"` + PastTriggersSeries *qbtypes.TimeSeries `json:"pastTriggersSeries" required:"true" nullable:"true"` + CurrentAvgResolutionTime float64 `json:"currentAvgResolutionTime" required:"true"` + PastAvgResolutionTime float64 `json:"pastAvgResolutionTime" required:"true"` + CurrentAvgResolutionTimeSeries *qbtypes.TimeSeries `json:"currentAvgResolutionTimeSeries" required:"true" nullable:"true"` + PastAvgResolutionTimeSeries *qbtypes.TimeSeries `json:"pastAvgResolutionTimeSeries" required:"true" nullable:"true"` +} diff --git a/pkg/types/rulestatehistorytypes/store.go b/pkg/types/rulestatehistorytypes/store.go new file mode 100644 index 00000000000..dade8bb3723 --- /dev/null +++ b/pkg/types/rulestatehistorytypes/store.go @@ -0,0 +1,124 @@ +package rulestatehistorytypes + +import ( + "context" + "database/sql/driver" + "encoding/json" + "sort" + "strings" + + "github.com/SigNoz/signoz/pkg/errors" + qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" + "github.com/SigNoz/signoz/pkg/types/telemetrytypes" + "github.com/SigNoz/signoz/pkg/valuer" +) + +type AlertState struct { + valuer.String +} + +var ( + StateInactive = AlertState{valuer.NewString("inactive")} + StatePending = AlertState{valuer.NewString("pending")} + StateRecovering = AlertState{valuer.NewString("recovering")} + StateFiring = AlertState{valuer.NewString("firing")} + StateNoData = AlertState{valuer.NewString("nodata")} + StateDisabled = AlertState{valuer.NewString("disabled")} +) + +type LabelsString string + +func (AlertState) Enum() []any { + return []any{ + StateInactive, + StatePending, + StateRecovering, + StateFiring, + StateNoData, + StateDisabled, + } +} + +func (l *LabelsString) Scan(src any) error { + switch data := src.(type) { + case nil: + *l = "" + case string: + *l = LabelsString(data) + case []byte: + *l = LabelsString(string(data)) + default: + return errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported labels type") + } + return nil +} + +func (l LabelsString) Value() (driver.Value, error) { + return string(l), nil +} + +func (l LabelsString) ToQBLabels() []*qbtypes.Label { + if strings.TrimSpace(string(l)) == "" { + return []*qbtypes.Label{} + } + + labelsMap := map[string]any{} + if err := json.Unmarshal([]byte(l), &labelsMap); err != nil { + return []*qbtypes.Label{} + } + + keys := make([]string, 0, len(labelsMap)) + for key := range labelsMap { + keys = append(keys, key) + } + sort.Strings(keys) + + labels := make([]*qbtypes.Label, 0, len(keys)) + for _, key := range keys { + labels = append(labels, &qbtypes.Label{ + Key: telemetrytypes.TelemetryFieldKey{ + Name: key, + }, + Value: labelsMap[key], + }) + } + + return labels +} + +type RuleStateHistory struct { + RuleID string `ch:"rule_id"` + RuleName string `ch:"rule_name"` + + OverallState AlertState `ch:"overall_state"` + OverallStateChanged bool `ch:"overall_state_changed"` + + State AlertState `ch:"state"` + StateChanged bool `ch:"state_changed"` + UnixMilli int64 `ch:"unix_milli"` + Labels LabelsString `ch:"labels"` + Fingerprint uint64 `ch:"fingerprint"` + Value float64 `ch:"value"` +} + +type RuleStateHistoryContributor struct { + Fingerprint uint64 `ch:"fingerprint"` + Labels LabelsString `ch:"labels"` + Count uint64 `ch:"count"` + RelatedTracesLink string + RelatedLogsLink string +} + +type Store interface { + AddRuleStateHistory(ctx context.Context, entries []RuleStateHistory) error + GetLastSavedRuleStateHistory(ctx context.Context, ruleID string) ([]RuleStateHistory, error) + ReadRuleStateHistoryByRuleID(ctx context.Context, ruleID string, query *Query) ([]RuleStateHistory, uint64, error) + ReadRuleStateHistoryFilterKeysByRuleID(ctx context.Context, ruleID string, query *Query, search string, limit int64) (*telemetrytypes.GettableFieldKeys, error) + ReadRuleStateHistoryFilterValuesByRuleID(ctx context.Context, ruleID string, key string, query *Query, search string, limit int64) (*telemetrytypes.GettableFieldValues, error) + ReadRuleStateHistoryTopContributorsByRuleID(ctx context.Context, ruleID string, query *Query) ([]RuleStateHistoryContributor, error) + GetOverallStateTransitions(ctx context.Context, ruleID string, query *Query) ([]GettableRuleStateWindow, error) + GetTotalTriggers(ctx context.Context, ruleID string, query *Query) (uint64, error) + GetTriggersByInterval(ctx context.Context, ruleID string, query *Query) (*qbtypes.TimeSeries, error) + GetAvgResolutionTime(ctx context.Context, ruleID string, query *Query) (float64, error) + GetAvgResolutionTimeByInterval(ctx context.Context, ruleID string, query *Query) (*qbtypes.TimeSeries, error) +} From b151bcd697d00513f79a4d67202341dad280fbcc Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Mon, 30 Mar 2026 16:20:34 +0530 Subject: [PATCH 48/78] chore(authz): add error logger for batch check (#10756) * chore(authz): add error logger for batch check * chore(authz): add error logger for batch check --- pkg/authz/openfgaserver/server.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pkg/authz/openfgaserver/server.go b/pkg/authz/openfgaserver/server.go index 77491e48072..b991d6241a4 100644 --- a/pkg/authz/openfgaserver/server.go +++ b/pkg/authz/openfgaserver/server.go @@ -18,6 +18,10 @@ import ( "google.golang.org/protobuf/encoding/protojson" ) +const ( + batchCheckItemErrorMessage = "::AUTHZ-CHECK-ERROR::" +) + var ( openfgaDefaultStore = valuer.NewString("signoz") ) @@ -126,6 +130,11 @@ func (server *Server) BatchCheck(ctx context.Context, tupleReq map[string]*openf response := make(map[string]*authtypes.TupleKeyAuthorization, len(tupleReq)) for id, tuple := range tupleReq { + // required because upstream doesn't set the error on the related spans: https://github.com/openfga/openfga/issues/3024 + if checkErr := checkResponse.Result[id].GetError(); checkErr != nil { + server.settings.Logger().ErrorContext(ctx, batchCheckItemErrorMessage, errors.Attr(server.getCheckError(checkErr))) + } + response[id] = &authtypes.TupleKeyAuthorization{ Tuple: tuple, Authorized: checkResponse.Result[id].GetAllowed(), @@ -341,3 +350,12 @@ func (server *Server) getStoreIDandModelID() (string, string) { return storeID, modelID } + +func (server *Server) getCheckError(checkErr *openfgav1.CheckError) error { + switch checkErr.GetCode().(type) { + case *openfgav1.CheckError_InputError: + return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, checkErr.GetMessage()) + default: + return errors.New(errors.TypeInternal, errors.CodeInternal, checkErr.GetMessage()) + } +} From 87e5ef2f0ad86415c85d016f322979ae110603d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vinicius=20Louren=C3=A7o?= <12551007+H4ad@users.noreply.github.com> Date: Mon, 30 Mar 2026 11:41:51 -0300 Subject: [PATCH 49/78] refactor(infrastructure-monitoring): use nuqs hooks (#10640) * fix(infra-monitoring): use nuqs instead of searchParams to handle decoding issues * fix(infra-monitoring): grouped row key trying to serialize circular reference * fix(hooks): use push instead of replace to preserve back/forward history * fix(infra-monitoring): preserve default order by from base query when value is null * fix(pr-comments): tests and unused code * fix(infra-monitoring): ensure all the pages uses/inherit base query order by * fix(jsons): remove mistaken files added by accident * fix(infra-monitoring): not resetting the filters after navigate * fix(search-params): remove searchParams hook and prefer just location.search --- frontend/package.json | 3 +- .../InfraMonitoringHosts/HostsList.tsx | 63 ++--- .../HostsListControls.tsx | 2 +- .../InfraMonitoringHosts/HostsListTable.tsx | 8 +- .../__tests__/HostsList.test.tsx | 58 ++--- .../__tests__/HostsListTable.test.tsx | 1 + .../container/InfraMonitoringHosts/utils.tsx | 22 +- .../ClusterDetails/ClusterDetails.tsx | 97 +++----- .../Clusters/K8sClustersList.tsx | 126 +++------- .../InfraMonitoringK8s/Clusters/utils.tsx | 3 +- .../DaemonSetDetails/DaemonSetDetails.tsx | 115 ++++----- .../DaemonSets/K8sDaemonSetsList.tsx | 130 +++------- .../InfraMonitoringK8s/DaemonSets/utils.tsx | 3 +- .../DeploymentDetails/DeploymentDetails.tsx | 94 +++----- .../Deployments/K8sDeploymentsList.tsx | 134 ++++------- .../InfraMonitoringK8s/Deployments/utils.tsx | 3 +- .../EntityEvents/entityEvents.styles.scss | 2 + .../EntityTraces/entityTraces.styles.scss | 2 + .../entityDetails.styles.scss | 4 + .../InfraMonitoringK8s/InfraMonitoringK8s.tsx | 34 ++- .../Jobs/JobDetails/JobDetails.tsx | 119 +++++----- .../InfraMonitoringK8s/Jobs/K8sJobsList.tsx | 126 +++------- .../InfraMonitoringK8s/Jobs/utils.tsx | 3 +- .../InfraMonitoringK8s/K8sHeader.tsx | 20 +- .../Namespaces/K8sNamespacesList.tsx | 133 ++++------- .../NamespaceDetails.styles.scss | 2 + .../NamespaceDetails/NamespaceDetails.tsx | 121 +++++----- .../InfraMonitoringK8s/Namespaces/utils.tsx | 3 +- .../InfraMonitoringK8s/Nodes/K8sNodesList.tsx | 137 ++++------- .../Nodes/NodeDetails/NodeDetails.tsx | 97 +++----- .../InfraMonitoringK8s/Nodes/utils.tsx | 3 +- .../InfraMonitoringK8s/Pods/K8sPodLists.tsx | 153 ++++-------- .../Pods/PodDetails/PodDetails.tsx | 98 +++----- .../StatefulSets/K8sStatefulSetsList.tsx | 135 ++++------- .../StatefulSetDetails/StatefulSetDetails.tsx | 117 ++++----- .../InfraMonitoringK8s/StatefulSets/utils.tsx | 3 +- .../Volumes/K8sVolumesList.tsx | 108 ++------- .../InfraMonitoringK8s/Volumes/utils.tsx | 5 +- .../ClusterDetails/ClusterDetails.test.tsx | 123 +++++----- .../DaemonSetDetails.test.tsx | 123 +++++----- .../DeploymentDetails.test.tsx | 123 +++++----- .../Jobs/JobDetails/JobDetails.test.tsx | 62 ++++- .../__tests__/K8sHeader.test.tsx | 131 ++++++++++ .../NamespaceDetails.test.tsx | 123 +++++----- .../Nodes/NodeDetails/NodeDetails.test.tsx | 103 +++++--- .../__tests__/Pods/K8sPodsList.test.tsx | 155 ++++++++++++ .../Pods/PodDetails/PodDetails.test.tsx | 83 ++++--- .../StatefulSetDetails.test.tsx | 56 ++--- .../__tests__/Volumes/K8sVolumesList.test.tsx | 19 +- .../__tests__/commonMocks.ts | 18 +- .../InfraMonitoringK8s/commonUtils.tsx | 32 +-- .../src/container/InfraMonitoringK8s/hooks.ts | 224 ++++++++++++++++++ .../container/InfraMonitoringK8s/schemas.ts | 10 + .../container/InfraMonitoringK8s/utils.tsx | 26 +- frontend/src/utils/nuqsParsers.ts | 16 ++ frontend/yarn.lock | 5 + 56 files changed, 1880 insertions(+), 1839 deletions(-) create mode 100644 frontend/src/container/InfraMonitoringK8s/__tests__/K8sHeader.test.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/__tests__/Pods/K8sPodsList.test.tsx create mode 100644 frontend/src/container/InfraMonitoringK8s/hooks.ts create mode 100644 frontend/src/container/InfraMonitoringK8s/schemas.ts create mode 100644 frontend/src/utils/nuqsParsers.ts diff --git a/frontend/package.json b/frontend/package.json index 10a561bd280..1342a0bc7c1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -164,6 +164,7 @@ "vite-plugin-html": "3.2.2", "web-vitals": "^0.2.4", "xstate": "^4.31.0", + "zod": "4.3.6", "zustand": "5.0.11" }, "browserslist": { @@ -286,4 +287,4 @@ "tmp": "0.2.4", "vite": "npm:rolldown-vite@7.3.1" } -} \ No newline at end of file +} diff --git a/frontend/src/container/InfraMonitoringHosts/HostsList.tsx b/frontend/src/container/InfraMonitoringHosts/HostsList.tsx index abfb4dfae3f..aa06537609d 100644 --- a/frontend/src/container/InfraMonitoringHosts/HostsList.tsx +++ b/frontend/src/container/InfraMonitoringHosts/HostsList.tsx @@ -1,7 +1,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; // eslint-disable-next-line no-restricted-imports import { useSelector } from 'react-redux'; -import { useSearchParams } from 'react-router-dom-v5-compat'; import { VerticalAlignTopOutlined } from '@ant-design/icons'; import { Button, Tooltip, Typography } from 'antd'; import logEvent from 'api/common/logEvent'; @@ -11,15 +10,16 @@ import QuickFilters from 'components/QuickFilters/QuickFilters'; import { QuickFiltersSource } from 'components/QuickFilters/types'; import { InfraMonitoringEvents } from 'constants/events'; import { - getFiltersFromParams, - getOrderByFromParams, -} from 'container/InfraMonitoringK8s/commonUtils'; -import { INFRA_MONITORING_K8S_PARAMS_KEYS } from 'container/InfraMonitoringK8s/constants'; + useInfraMonitoringCurrentPage, + useInfraMonitoringFiltersHosts, + useInfraMonitoringOrderByHosts, +} from 'container/InfraMonitoringK8s/hooks'; import { usePageSize } from 'container/InfraMonitoringK8s/utils'; import { useGetHostList } from 'hooks/infraMonitoring/useGetHostList'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations'; import { Filter } from 'lucide-react'; +import { parseAsString, useQueryState } from 'nuqs'; import { AppState } from 'store/reducers'; import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData'; import { GlobalReducer } from 'types/reducer/globalTime'; @@ -35,50 +35,29 @@ function HostsList(): JSX.Element { const { maxTime, minTime } = useSelector( (state) => state.globalTime, ); - const [searchParams, setSearchParams] = useSearchParams(); - const [currentPage, setCurrentPage] = useState(1); - const [filters, setFilters] = useState(() => { - const filters = getFiltersFromParams( - searchParams, - INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS, - ); - if (!filters) { - return { - items: [], - op: 'and', - }; - } - return filters; - }); + const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage(); + const [filters, setFilters] = useInfraMonitoringFiltersHosts(); + const [orderBy, setOrderBy] = useInfraMonitoringOrderByHosts(); + const [showFilters, setShowFilters] = useState(true); - const [orderBy, setOrderBy] = useState<{ - columnName: string; - order: 'asc' | 'desc'; - } | null>(() => getOrderByFromParams(searchParams)); + const [selectedHostName, setSelectedHostName] = useQueryState( + 'hostName', + parseAsString.withDefault(''), + ); const handleOrderByChange = ( - orderBy: { + orderByValue: { columnName: string; order: 'asc' | 'desc'; } | null, ): void => { - setOrderBy(orderBy); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(orderBy), - }); + setOrderBy(orderByValue); }; - const [selectedHostName, setSelectedHostName] = useState(() => { - const hostName = searchParams.get('hostName'); - return hostName || null; - }); - const handleHostClick = (hostName: string): void => { setSelectedHostName(hostName); - setSearchParams({ ...searchParams, hostName }); }; const { pageSize, setPageSize } = usePageSize('hosts'); @@ -154,12 +133,8 @@ function HostsList(): JSX.Element { const handleFiltersChange = useCallback( (value: IBuilderQuery['filters']): void => { const isNewFilterAdded = value?.items?.length !== filters?.items?.length; - setFilters(value); + setFilters(value ?? null); handleChangeQueryData('filters', value); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS]: JSON.stringify(value), - }); if (isNewFilterAdded) { setCurrentPage(1); @@ -171,8 +146,7 @@ function HostsList(): JSX.Element { } } }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [filters], + [filters, setFilters, setCurrentPage, handleChangeQueryData], ); useEffect(() => { @@ -184,7 +158,7 @@ function HostsList(): JSX.Element { }, [data?.payload?.data?.total]); const selectedHostData = useMemo(() => { - if (!selectedHostName) { + if (!selectedHostName?.trim()) { return null; } return ( @@ -260,6 +234,7 @@ function HostsList(): JSX.Element { pageSize={pageSize} setPageSize={setPageSize} setOrderBy={handleOrderByChange} + orderBy={orderBy} />
diff --git a/frontend/src/container/InfraMonitoringHosts/HostsListControls.tsx b/frontend/src/container/InfraMonitoringHosts/HostsListControls.tsx index 9b4ae9067d8..29045425366 100644 --- a/frontend/src/container/InfraMonitoringHosts/HostsListControls.tsx +++ b/frontend/src/container/InfraMonitoringHosts/HostsListControls.tsx @@ -14,7 +14,7 @@ function HostsListControls({ showAutoRefresh, }: { handleFiltersChange: (value: IBuilderQuery['filters']) => void; - filters: IBuilderQuery['filters']; + filters: IBuilderQuery['filters'] | null; showAutoRefresh: boolean; }): JSX.Element { const currentQuery = initialQueriesMap[DataSource.METRICS]; diff --git a/frontend/src/container/InfraMonitoringHosts/HostsListTable.tsx b/frontend/src/container/InfraMonitoringHosts/HostsListTable.tsx index 75ff5eef8d0..5a226a674c3 100644 --- a/frontend/src/container/InfraMonitoringHosts/HostsListTable.tsx +++ b/frontend/src/container/InfraMonitoringHosts/HostsListTable.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { LoadingOutlined } from '@ant-design/icons'; import { Skeleton, @@ -116,8 +116,12 @@ export default function HostsListTable({ pageSize, setOrderBy, setPageSize, + orderBy, }: HostsListTableProps): JSX.Element { - const columns = useMemo(() => getHostsListColumns(), []); + const [defaultOrderBy] = useState(orderBy); + const columns = useMemo(() => getHostsListColumns(defaultOrderBy), [ + defaultOrderBy, + ]); const sentAnyHostMetricsData = useMemo( () => data?.payload?.data?.sentAnyHostMetricsData || false, diff --git a/frontend/src/container/InfraMonitoringHosts/__tests__/HostsList.test.tsx b/frontend/src/container/InfraMonitoringHosts/__tests__/HostsList.test.tsx index 143fb43815f..f1e0624175e 100644 --- a/frontend/src/container/InfraMonitoringHosts/__tests__/HostsList.test.tsx +++ b/frontend/src/container/InfraMonitoringHosts/__tests__/HostsList.test.tsx @@ -4,6 +4,7 @@ import { Provider } from 'react-redux'; import { MemoryRouter } from 'react-router-dom'; import { render } from '@testing-library/react'; import * as useGetHostListHooks from 'hooks/infraMonitoring/useGetHostList'; +import { withNuqsTestingAdapter } from 'nuqs/adapters/testing'; import * as appContextHooks from 'providers/App/App'; import * as timezoneHooks from 'providers/Timezone'; import store from 'store'; @@ -19,6 +20,16 @@ jest.mock('lib/getMinMax', () => ({ isValidShortHandDateTimeFormat: jest.fn().mockReturnValue(true), })), })); +jest.mock('container/TopNav/DateTimeSelectionV2', () => ({ + __esModule: true, + default: (): JSX.Element => ( +
Date Time
+ ), +})); +jest.mock('components/HostMetricsDetail', () => ({ + __esModule: true, + default: (): null => null, +})); jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({ __esModule: true, default: ({ onSelect, selectedTime, selectedValue }: any): JSX.Element => ( @@ -55,19 +66,6 @@ jest.mock('react-router-dom', () => { }), }; }); -jest.mock('react-router-dom-v5-compat', () => { - const actual = jest.requireActual('react-router-dom-v5-compat'); - return { - ...actual, - useSearchParams: jest - .fn() - .mockReturnValue([ - { get: jest.fn(), entries: jest.fn().mockReturnValue([]) }, - jest.fn(), - ]), - useNavigationType: (): any => 'PUSH', - }; -}); jest.mock('hooks/useSafeNavigate', () => ({ useSafeNavigate: (): any => ({ safeNavigate: jest.fn(), @@ -127,29 +125,35 @@ jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({ }, } as any); +const Wrapper = withNuqsTestingAdapter({ searchParams: {} }); + describe('HostsList', () => { it('renders hosts list table', () => { const { container } = render( - - - - - - - , + + + + + + + + + , ); expect(container.querySelector('.hosts-list-table')).toBeInTheDocument(); }); it('renders filters', () => { const { container } = render( - - - - - - - , + + + + + + + + + , ); expect(container.querySelector('.filters')).toBeInTheDocument(); }); diff --git a/frontend/src/container/InfraMonitoringHosts/__tests__/HostsListTable.test.tsx b/frontend/src/container/InfraMonitoringHosts/__tests__/HostsListTable.test.tsx index f809edc1958..5b378b8b9ef 100644 --- a/frontend/src/container/InfraMonitoringHosts/__tests__/HostsListTable.test.tsx +++ b/frontend/src/container/InfraMonitoringHosts/__tests__/HostsListTable.test.tsx @@ -72,6 +72,7 @@ describe('HostsListTable', () => { pageSize: 10, setOrderBy: mockSetOrderBy, setPageSize: mockSetPageSize, + orderBy: null, }; it('renders loading state if isLoading is true and tableData is empty', () => { diff --git a/frontend/src/container/InfraMonitoringHosts/utils.tsx b/frontend/src/container/InfraMonitoringHosts/utils.tsx index 9fadc531953..4b4ec7cfc95 100644 --- a/frontend/src/container/InfraMonitoringHosts/utils.tsx +++ b/frontend/src/container/InfraMonitoringHosts/utils.tsx @@ -3,6 +3,7 @@ import { InfoCircleOutlined } from '@ant-design/icons'; import { Color } from '@signozhq/design-tokens'; import { Progress, TabsProps, Tag, Tooltip, Typography } from 'antd'; import { TableColumnType as ColumnType } from 'antd'; +import { SortOrder } from 'antd/lib/table/interface'; import { HostData, HostListPayload, @@ -20,6 +21,7 @@ import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { TagFilter } from 'types/api/queryBuilder/queryBuilderData'; import { DataSource } from 'types/common/queryBuilder'; +import { OrderBySchemaType } from '../InfraMonitoringK8s/schemas'; import HostsList from './HostsList'; import './InfraMonitoring.styles.scss'; @@ -105,6 +107,7 @@ export interface HostsListTableProps { orderBy: { columnName: string; order: 'asc' | 'desc' } | null, ) => void; setPageSize: (pageSize: number) => void; + orderBy: OrderBySchemaType; } export interface EmptyOrLoadingViewProps { @@ -127,6 +130,17 @@ export const getHostListsQuery = (): HostListPayload => ({ orderBy: { columnName: 'cpu', order: 'desc' }, }); +function mapOrderByToSortOrder( + column: string, + orderBy: OrderBySchemaType, +): SortOrder | undefined { + return orderBy?.columnName === column + ? orderBy?.order === 'asc' + ? 'ascend' + : 'descend' + : undefined; +} + export const getTabsItems = (): TabsProps['items'] => [ { label: , @@ -135,7 +149,9 @@ export const getTabsItems = (): TabsProps['items'] => [ }, ]; -export const getHostsListColumns = (): ColumnType[] => [ +export const getHostsListColumns = ( + orderBy: OrderBySchemaType, +): ColumnType[] => [ { title:
Hostname
, dataIndex: 'hostName', @@ -164,6 +180,7 @@ export const getHostsListColumns = (): ColumnType[] => [ key: 'cpu', width: 100, sorter: true, + defaultSortOrder: mapOrderByToSortOrder('cpu', orderBy), align: 'right', }, { @@ -179,6 +196,7 @@ export const getHostsListColumns = (): ColumnType[] => [ key: 'memory', width: 100, sorter: true, + defaultSortOrder: mapOrderByToSortOrder('memory', orderBy), align: 'right', }, { @@ -187,6 +205,7 @@ export const getHostsListColumns = (): ColumnType[] => [ key: 'wait', width: 100, sorter: true, + defaultSortOrder: mapOrderByToSortOrder('wait', orderBy), align: 'right', }, { @@ -195,6 +214,7 @@ export const getHostsListColumns = (): ColumnType[] => [ key: 'load15', width: 100, sorter: true, + defaultSortOrder: mapOrderByToSortOrder('load15', orderBy), align: 'right', }, ]; diff --git a/frontend/src/container/InfraMonitoringK8s/Clusters/ClusterDetails/ClusterDetails.tsx b/frontend/src/container/InfraMonitoringK8s/Clusters/ClusterDetails/ClusterDetails.tsx index 4e2481fc73e..22ad9eaa777 100644 --- a/frontend/src/container/InfraMonitoringK8s/Clusters/ClusterDetails/ClusterDetails.tsx +++ b/frontend/src/container/InfraMonitoringK8s/Clusters/ClusterDetails/ClusterDetails.tsx @@ -1,7 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; // eslint-disable-next-line no-restricted-imports import { useSelector } from 'react-redux'; -import { useSearchParams } from 'react-router-dom-v5-compat'; import { Color, Spacing } from '@signozhq/design-tokens'; import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd'; import type { RadioChangeEvent } from 'antd/lib'; @@ -15,15 +14,15 @@ import { initialQueryState, } from 'constants/queryBuilder'; import ROUTES from 'constants/routes'; -import { - filterDuplicateFilters, - getFiltersFromParams, -} from 'container/InfraMonitoringK8s/commonUtils'; -import { - INFRA_MONITORING_K8S_PARAMS_KEYS, - K8sCategory, -} from 'container/InfraMonitoringK8s/constants'; +import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils'; +import { K8sCategory } from 'container/InfraMonitoringK8s/constants'; import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/utils'; +import { + useInfraMonitoringEventsFilters, + useInfraMonitoringLogFilters, + useInfraMonitoringTracesFilters, + useInfraMonitoringView, +} from 'container/InfraMonitoringK8s/hooks'; import { CustomTimeType, Time, @@ -93,23 +92,21 @@ function ClusterDetails({ : (selectedTime as Time), ); - const [searchParams, setSearchParams] = useSearchParams(); - const [selectedView, setSelectedView] = useState(() => { - const view = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW); - if (view) { - return view as VIEWS; - } - return VIEWS.METRICS; - }); + const [selectedView, setSelectedView] = useInfraMonitoringView(); + const [logFiltersParam, setLogFiltersParam] = useInfraMonitoringLogFilters(); + const [ + tracesFiltersParam, + setTracesFiltersParam, + ] = useInfraMonitoringTracesFilters(); + const [ + eventsFiltersParam, + setEventsFiltersParam, + ] = useInfraMonitoringEventsFilters(); const isDarkMode = useIsDarkMode(); const initialFilters = useMemo(() => { - const urlView = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW); - const queryKey = - urlView === VIEW_TYPES.LOGS - ? INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS - : INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS; - const filters = getFiltersFromParams(searchParams, queryKey); + const filters = + selectedView === VIEW_TYPES.LOGS ? logFiltersParam : tracesFiltersParam; if (filters) { return filters; } @@ -129,15 +126,16 @@ function ClusterDetails({ }, ], }; - }, [cluster?.meta.k8s_cluster_name, searchParams]); + }, [ + cluster?.meta.k8s_cluster_name, + selectedView, + logFiltersParam, + tracesFiltersParam, + ]); const initialEventsFilters = useMemo(() => { - const filters = getFiltersFromParams( - searchParams, - INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS, - ); - if (filters) { - return filters; + if (eventsFiltersParam) { + return eventsFiltersParam; } return { op: 'AND', @@ -166,7 +164,7 @@ function ClusterDetails({ }, ], }; - }, [cluster?.meta.k8s_cluster_name, searchParams]); + }, [cluster?.meta.k8s_cluster_name, eventsFiltersParam]); const [logsAndTracesFilters, setLogsAndTracesFilters] = useState< IBuilderQuery['filters'] @@ -207,13 +205,9 @@ function ClusterDetails({ const handleTabChange = (e: RadioChangeEvent): void => { setSelectedView(e.target.value); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: e.target.value, - [INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(null), - [INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(null), - [INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(null), - }); + setLogFiltersParam(null); + setTracesFiltersParam(null); + setEventsFiltersParam(null); logEvent(InfraMonitoringEvents.TabChanged, { entity: InfraMonitoringEvents.K8sEntity, page: InfraMonitoringEvents.DetailedPage, @@ -287,13 +281,8 @@ function ClusterDetails({ ), }; - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify( - updatedFilters, - ), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view, - }); + setLogFiltersParam(updatedFilters); + setSelectedView(view); return updatedFilters; }); @@ -330,13 +319,8 @@ function ClusterDetails({ ), }; - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify( - updatedFilters, - ), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view, - }); + setTracesFiltersParam(updatedFilters); + setSelectedView(view); return updatedFilters; }); @@ -379,13 +363,8 @@ function ClusterDetails({ ), }; - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify( - updatedFilters, - ), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view, - }); + setEventsFiltersParam(updatedFilters); + setSelectedView(view); return updatedFilters; }); diff --git a/frontend/src/container/InfraMonitoringK8s/Clusters/K8sClustersList.tsx b/frontend/src/container/InfraMonitoringK8s/Clusters/K8sClustersList.tsx index 03f32062903..2bb522be19b 100644 --- a/frontend/src/container/InfraMonitoringK8s/Clusters/K8sClustersList.tsx +++ b/frontend/src/container/InfraMonitoringK8s/Clusters/K8sClustersList.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; // eslint-disable-next-line no-restricted-imports -import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly -import { useSearchParams } from 'react-router-dom-v5-compat'; +import { useSelector } from 'react-redux'; import { LoadingOutlined } from '@ant-design/icons'; import { Button, @@ -29,12 +28,17 @@ import { openInNewTab } from 'utils/navigation'; import { FeatureKeys } from '../../../constants/features'; import { useAppContext } from '../../../providers/App/App'; -import { getOrderByFromParams } from '../commonUtils'; import { GetK8sEntityToAggregateAttribute, INFRA_MONITORING_K8S_PARAMS_KEYS, K8sCategory, } from '../constants'; +import { + useInfraMonitoringClusterName, + useInfraMonitoringCurrentPage, + useInfraMonitoringGroupBy, + useInfraMonitoringOrderBy, +} from '../hooks'; import K8sHeader from '../K8sHeader'; import LoadingContainer from '../LoadingContainer'; import { usePageSize } from '../utils'; @@ -49,6 +53,7 @@ import { import '../InfraMonitoringK8s.styles.scss'; import './K8sClustersList.styles.scss'; + function K8sClustersList({ isFiltersVisible, handleFilterVisibilityChange, @@ -62,55 +67,19 @@ function K8sClustersList({ (state) => state.globalTime, ); - const [searchParams, setSearchParams] = useSearchParams(); + const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage(); + const [groupBy, setGroupBy] = useInfraMonitoringGroupBy(); + const [orderBy, setOrderBy] = useInfraMonitoringOrderBy(); + const [ + selectedClusterName, + setselectedClusterName, + ] = useInfraMonitoringClusterName(); - const [currentPage, setCurrentPage] = useState(() => { - const page = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE); - if (page) { - return parseInt(page, 10); - } - return 1; - }); const [filtersInitialised, setFiltersInitialised] = useState(false); const [expandedRowKeys, setExpandedRowKeys] = useState([]); - useEffect(() => { - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE]: currentPage.toString(), - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentPage]); - - const [orderBy, setOrderBy] = useState<{ - columnName: string; - order: 'asc' | 'desc'; - } | null>(() => getOrderByFromParams(searchParams, false)); - - const [selectedClusterName, setselectedClusterName] = useState( - () => { - const clusterName = searchParams.get( - INFRA_MONITORING_K8S_PARAMS_KEYS.CLUSTER_NAME, - ); - if (clusterName) { - return clusterName; - } - return null; - }, - ); - const { pageSize, setPageSize } = usePageSize(K8sCategory.CLUSTERS); - const [groupBy, setGroupBy] = useState(() => { - const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY); - if (groupBy) { - const decoded = decodeURIComponent(groupBy); - const parsed = JSON.parse(decoded); - return parsed as IBuilderQuery['groupBy']; - } - return []; - }); - const [ selectedRowData, setSelectedRowData, @@ -136,7 +105,7 @@ function K8sClustersList({ if (quickFiltersLastUpdated !== -1) { setCurrentPage(1); } - }, [quickFiltersLastUpdated]); + }, [quickFiltersLastUpdated, setCurrentPage]); const { featureFlags } = useAppContext(); const dotMetricsEnabled = @@ -189,25 +158,28 @@ function K8sClustersList({ filters, start: Math.floor(minTime / 1000000), end: Math.floor(maxTime / 1000000), - orderBy, + orderBy: orderBy || baseQuery.orderBy, }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [minTime, maxTime, orderBy, selectedRowData, groupBy]); const groupedByRowDataQueryKey = useMemo(() => { + // be careful with what you serialize from selectedRowData + // since it's react node, it could contain circular references + const selectedRowDataKey = JSON.stringify(selectedRowData?.groupedByMeta); if (selectedClusterName) { return [ 'clusterList', JSON.stringify(queryFilters), JSON.stringify(orderBy), - JSON.stringify(selectedRowData), + selectedRowDataKey, ]; } return [ 'clusterList', JSON.stringify(queryFilters), JSON.stringify(orderBy), - JSON.stringify(selectedRowData), + selectedRowDataKey, String(minTime), String(maxTime), ]; @@ -266,7 +238,7 @@ function K8sClustersList({ filters: queryFilters, start: Math.floor(minTime / 1000000), end: Math.floor(maxTime / 1000000), - orderBy, + orderBy: orderBy || baseQuery.orderBy, }; if (groupBy.length > 0) { queryPayload.groupBy = groupBy; @@ -374,26 +346,15 @@ function K8sClustersList({ } if ('field' in sorter && sorter.order) { - const currentOrderBy = { + setOrderBy({ columnName: sorter.field as string, order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc', - }; - setOrderBy(currentOrderBy); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify( - currentOrderBy, - ), }); } else { setOrderBy(null); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null), - }); } }, - [searchParams, setSearchParams], + [setCurrentPage, setOrderBy], ); const { handleChangeQueryData } = useQueryOperations({ @@ -453,7 +414,7 @@ function K8sClustersList({ }, [selectedClusterName, groupBy.length, clustersData, nestedClustersData]); const openClusterInNewTab = (record: K8sClustersRowData): void => { - const newParams = new URLSearchParams(searchParams); + const newParams = new URLSearchParams(document.location.search); newParams.set( INFRA_MONITORING_K8S_PARAMS_KEYS.CLUSTER_NAME, record.clusterUID, @@ -477,10 +438,6 @@ function K8sClustersList({ if (groupBy.length === 0) { setSelectedRowData(null); setselectedClusterName(record.clusterUID); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.CLUSTER_NAME]: record.clusterUID, - }); } else { handleGroupByRowClick(record); } @@ -509,11 +466,6 @@ function K8sClustersList({ setSelectedRowData(null); setGroupBy([]); setOrderBy(null); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify([]), - [INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null), - }); }; const expandedRowRender = (): JSX.Element => ( @@ -610,25 +562,11 @@ function K8sClustersList({ const handleCloseClusterDetail = (): void => { setselectedClusterName(null); - setSearchParams({ - ...Object.fromEntries( - Array.from(searchParams.entries()).filter( - ([key]) => - ![ - INFRA_MONITORING_K8S_PARAMS_KEYS.CLUSTER_NAME, - INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW, - INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS, - INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS, - INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS, - ].includes(key), - ), - ), - }); }; const handleGroupByChange = useCallback( (value: IBuilderQuery['groupBy']) => { - const groupBy = []; + const newGroupBy = []; for (let index = 0; index < value.length; index++) { const element = (value[index] as unknown) as string; @@ -638,17 +576,13 @@ function K8sClustersList({ ); if (key) { - groupBy.push(key); + newGroupBy.push(key); } } // Reset pagination on switching to groupBy setCurrentPage(1); - setGroupBy(groupBy); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify(groupBy), - }); + setGroupBy(newGroupBy); setExpandedRowKeys([]); logEvent(InfraMonitoringEvents.GroupByChanged, { entity: InfraMonitoringEvents.K8sEntity, @@ -656,7 +590,7 @@ function K8sClustersList({ category: InfraMonitoringEvents.Cluster, }); }, - [groupByFiltersData, searchParams, setSearchParams], + [groupByFiltersData, setCurrentPage, setGroupBy], ); useEffect(() => { diff --git a/frontend/src/container/InfraMonitoringK8s/Clusters/utils.tsx b/frontend/src/container/InfraMonitoringK8s/Clusters/utils.tsx index 6c35e902356..71d7bf849cc 100644 --- a/frontend/src/container/InfraMonitoringK8s/Clusters/utils.tsx +++ b/frontend/src/container/InfraMonitoringK8s/Clusters/utils.tsx @@ -1,6 +1,5 @@ import { Color } from '@signozhq/design-tokens'; -import { Tag, Tooltip } from 'antd'; -import { TableColumnType as ColumnType } from 'antd'; +import { TableColumnType as ColumnType, Tag, Tooltip } from 'antd'; import { K8sClustersData, K8sClustersListPayload, diff --git a/frontend/src/container/InfraMonitoringK8s/DaemonSets/DaemonSetDetails/DaemonSetDetails.tsx b/frontend/src/container/InfraMonitoringK8s/DaemonSets/DaemonSetDetails/DaemonSetDetails.tsx index 8e2b0a8492e..89196cfd4a2 100644 --- a/frontend/src/container/InfraMonitoringK8s/DaemonSets/DaemonSetDetails/DaemonSetDetails.tsx +++ b/frontend/src/container/InfraMonitoringK8s/DaemonSets/DaemonSetDetails/DaemonSetDetails.tsx @@ -2,7 +2,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; // eslint-disable-next-line no-restricted-imports import { useSelector } from 'react-redux'; -import { useSearchParams } from 'react-router-dom-v5-compat'; import { Color, Spacing } from '@signozhq/design-tokens'; import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd'; import type { RadioChangeEvent } from 'antd/lib'; @@ -15,11 +14,14 @@ import { initialQueryState, } from 'constants/queryBuilder'; import ROUTES from 'constants/routes'; -import { getFiltersFromParams } from 'container/InfraMonitoringK8s/commonUtils'; +import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils'; +import { K8sCategory } from 'container/InfraMonitoringK8s/constants'; import { - INFRA_MONITORING_K8S_PARAMS_KEYS, - K8sCategory, -} from 'container/InfraMonitoringK8s/constants'; + useInfraMonitoringEventsFilters, + useInfraMonitoringLogFilters, + useInfraMonitoringTracesFilters, + useInfraMonitoringView, +} from 'container/InfraMonitoringK8s/hooks'; import { CustomTimeType, Time, @@ -93,23 +95,21 @@ function DaemonSetDetails({ : (selectedTime as Time), ); - const [searchParams, setSearchParams] = useSearchParams(); - const [selectedView, setSelectedView] = useState(() => { - const view = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW); - if (view) { - return view as VIEWS; - } - return VIEWS.METRICS; - }); + const [selectedView, setSelectedView] = useInfraMonitoringView(); + const [logFiltersParam, setLogFiltersParam] = useInfraMonitoringLogFilters(); + const [ + tracesFiltersParam, + setTracesFiltersParam, + ] = useInfraMonitoringTracesFilters(); + const [ + eventsFiltersParam, + setEventsFiltersParam, + ] = useInfraMonitoringEventsFilters(); const isDarkMode = useIsDarkMode(); const initialFilters = useMemo(() => { - const urlView = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW); - const queryKey = - urlView === VIEW_TYPES.LOGS - ? INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS - : INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS; - const filters = getFiltersFromParams(searchParams, queryKey); + const filters = + selectedView === VIEW_TYPES.LOGS ? logFiltersParam : tracesFiltersParam; if (filters) { return filters; } @@ -143,16 +143,14 @@ function DaemonSetDetails({ }, [ daemonSet?.meta.k8s_daemonset_name, daemonSet?.meta.k8s_namespace_name, - searchParams, + selectedView, + logFiltersParam, + tracesFiltersParam, ]); const initialEventsFilters = useMemo(() => { - const filters = getFiltersFromParams( - searchParams, - INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS, - ); - if (filters) { - return filters; + if (eventsFiltersParam) { + return eventsFiltersParam; } return { op: 'AND', @@ -181,7 +179,7 @@ function DaemonSetDetails({ }, ], }; - }, [daemonSet?.meta.k8s_daemonset_name, searchParams]); + }, [daemonSet?.meta.k8s_daemonset_name, eventsFiltersParam]); const [logAndTracesFilters, setLogAndTracesFilters] = useState< IBuilderQuery['filters'] @@ -222,13 +220,9 @@ function DaemonSetDetails({ const handleTabChange = (e: RadioChangeEvent): void => { setSelectedView(e.target.value); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: e.target.value, - [INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(null), - [INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(null), - [INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(null), - }); + setLogFiltersParam(null); + setTracesFiltersParam(null); + setEventsFiltersParam(null); logEvent(InfraMonitoringEvents.TabChanged, { entity: InfraMonitoringEvents.K8sEntity, page: InfraMonitoringEvents.DetailedPage, @@ -296,20 +290,17 @@ function DaemonSetDetails({ const updatedFilters = { op: 'AND', - items: [ - ...(primaryFilters || []), - ...(newFilters || []), - ...(paginationFilter ? [paginationFilter] : []), - ].filter((item): item is TagFilterItem => item !== undefined), + items: filterDuplicateFilters( + [ + ...(primaryFilters || []), + ...(newFilters || []), + ...(paginationFilter ? [paginationFilter] : []), + ].filter((item): item is TagFilterItem => item !== undefined), + ), }; - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify( - updatedFilters, - ), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view, - }); + setLogFiltersParam(updatedFilters); + setSelectedView(view); return updatedFilters; }); }, @@ -337,21 +328,18 @@ function DaemonSetDetails({ const updatedFilters = { op: 'AND', - items: [ - ...(primaryFilters || []), - ...(value?.items?.filter( - (item) => item.key?.key !== QUERY_KEYS.K8S_DAEMON_SET_NAME, - ) || []), - ].filter((item): item is TagFilterItem => item !== undefined), + items: filterDuplicateFilters( + [ + ...(primaryFilters || []), + ...(value?.items?.filter( + (item) => item.key?.key !== QUERY_KEYS.K8S_DAEMON_SET_NAME, + ) || []), + ].filter((item): item is TagFilterItem => item !== undefined), + ), }; - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify( - updatedFilters, - ), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view, - }); + setTracesFiltersParam(updatedFilters); + setSelectedView(view); return updatedFilters; }); @@ -392,13 +380,8 @@ function DaemonSetDetails({ ].filter((item): item is TagFilterItem => item !== undefined), }; - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify( - updatedFilters, - ), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view, - }); + setEventsFiltersParam(updatedFilters); + setSelectedView(view); return updatedFilters; }); diff --git a/frontend/src/container/InfraMonitoringK8s/DaemonSets/K8sDaemonSetsList.tsx b/frontend/src/container/InfraMonitoringK8s/DaemonSets/K8sDaemonSetsList.tsx index a61de131031..dbadfaf83a7 100644 --- a/frontend/src/container/InfraMonitoringK8s/DaemonSets/K8sDaemonSetsList.tsx +++ b/frontend/src/container/InfraMonitoringK8s/DaemonSets/K8sDaemonSetsList.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; // eslint-disable-next-line no-restricted-imports -import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly -import { useSearchParams } from 'react-router-dom-v5-compat'; +import { useSelector } from 'react-redux'; import { LoadingOutlined } from '@ant-design/icons'; import { Button, @@ -30,12 +29,21 @@ import { openInNewTab } from 'utils/navigation'; import { FeatureKeys } from '../../../constants/features'; import { useAppContext } from '../../../providers/App/App'; -import { getOrderByFromParams } from '../commonUtils'; import { GetK8sEntityToAggregateAttribute, INFRA_MONITORING_K8S_PARAMS_KEYS, K8sCategory, } from '../constants'; +import { + useInfraMonitoringCurrentPage, + useInfraMonitoringDaemonSetUID, + useInfraMonitoringEventsFilters, + useInfraMonitoringGroupBy, + useInfraMonitoringLogFilters, + useInfraMonitoringOrderBy, + useInfraMonitoringTracesFilters, + useInfraMonitoringView, +} from '../hooks'; import K8sHeader from '../K8sHeader'; import LoadingContainer from '../LoadingContainer'; import { usePageSize } from '../utils'; @@ -64,54 +72,25 @@ function K8sDaemonSetsList({ (state) => state.globalTime, ); - const [searchParams, setSearchParams] = useSearchParams(); - - const [currentPage, setCurrentPage] = useState(() => { - const page = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE); - if (page) { - return parseInt(page, 10); - } - return 1; - }); + const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage(); const [filtersInitialised, setFiltersInitialised] = useState(false); const [expandedRowKeys, setExpandedRowKeys] = useState([]); - useEffect(() => { - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE]: currentPage.toString(), - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentPage]); - - const [orderBy, setOrderBy] = useState<{ - columnName: string; - order: 'asc' | 'desc'; - } | null>(() => getOrderByFromParams(searchParams, true)); + const [orderBy, setOrderBy] = useInfraMonitoringOrderBy(); - const [selectedDaemonSetUID, setSelectedDaemonSetUID] = useState< - string | null - >(() => { - const daemonSetUID = searchParams.get( - INFRA_MONITORING_K8S_PARAMS_KEYS.DAEMONSET_UID, - ); - if (daemonSetUID) { - return daemonSetUID; - } - return null; - }); + const [ + selectedDaemonSetUID, + setSelectedDaemonSetUID, + ] = useInfraMonitoringDaemonSetUID(); const { pageSize, setPageSize } = usePageSize(K8sCategory.DAEMONSETS); - const [groupBy, setGroupBy] = useState(() => { - const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY); - if (groupBy) { - const decoded = decodeURIComponent(groupBy); - const parsed = JSON.parse(decoded); - return parsed as IBuilderQuery['groupBy']; - } - return []; - }); + const [groupBy, setGroupBy] = useInfraMonitoringGroupBy(); + + const [, setView] = useInfraMonitoringView(); + const [, setTracesFilters] = useInfraMonitoringTracesFilters(); + const [, setEventsFilters] = useInfraMonitoringEventsFilters(); + const [, setLogFilters] = useInfraMonitoringLogFilters(); const [ selectedRowData, @@ -138,7 +117,7 @@ function K8sDaemonSetsList({ if (quickFiltersLastUpdated !== -1) { setCurrentPage(1); } - }, [quickFiltersLastUpdated]); + }, [quickFiltersLastUpdated, setCurrentPage]); const { featureFlags } = useAppContext(); const dotMetricsEnabled = @@ -191,25 +170,28 @@ function K8sDaemonSetsList({ filters, start: Math.floor(minTime / 1000000), end: Math.floor(maxTime / 1000000), - orderBy, + orderBy: orderBy || baseQuery.orderBy, }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [minTime, maxTime, orderBy, selectedRowData, groupBy]); const groupedByRowDataQueryKey = useMemo(() => { + // be careful with what you serialize from selectedRowData + // since it's react node, it could contain circular references + const selectedRowDataKey = JSON.stringify(selectedRowData?.groupedByMeta); if (selectedDaemonSetUID) { return [ 'daemonSetList', JSON.stringify(queryFilters), JSON.stringify(orderBy), - JSON.stringify(selectedRowData), + selectedRowDataKey, ]; } return [ 'daemonSetList', JSON.stringify(queryFilters), JSON.stringify(orderBy), - JSON.stringify(selectedRowData), + selectedRowDataKey, String(minTime), String(maxTime), ]; @@ -268,7 +250,7 @@ function K8sDaemonSetsList({ filters: queryFilters, start: Math.floor(minTime / 1000000), end: Math.floor(maxTime / 1000000), - orderBy, + orderBy: orderBy || baseQuery.orderBy, }; if (groupBy.length > 0) { queryPayload.groupBy = groupBy; @@ -378,26 +360,15 @@ function K8sDaemonSetsList({ } if ('field' in sorter && sorter.order) { - const currentOrderBy = { + setOrderBy({ columnName: sorter.field as string, order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc', - }; - setOrderBy(currentOrderBy); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify( - currentOrderBy, - ), }); } else { setOrderBy(null); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null), - }); } }, - [searchParams, setSearchParams], + [setCurrentPage, setOrderBy], ); const { handleChangeQueryData } = useQueryOperations({ @@ -459,7 +430,7 @@ function K8sDaemonSetsList({ ]); const openDaemonSetInNewTab = (record: K8sDaemonSetsRowData): void => { - const newParams = new URLSearchParams(searchParams); + const newParams = new URLSearchParams(document.location.search); newParams.set( INFRA_MONITORING_K8S_PARAMS_KEYS.DAEMONSET_UID, record.daemonsetUID, @@ -483,10 +454,6 @@ function K8sDaemonSetsList({ if (groupBy.length === 0) { setSelectedRowData(null); setSelectedDaemonSetUID(record.daemonsetUID); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.DAEMONSET_UID]: record.daemonsetUID, - }); } else { handleGroupByRowClick(record); } @@ -515,11 +482,6 @@ function K8sDaemonSetsList({ setSelectedRowData(null); setGroupBy([]); setOrderBy(null); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify([]), - [INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null), - }); }; const expandedRowRender = (): JSX.Element => ( @@ -616,20 +578,10 @@ function K8sDaemonSetsList({ const handleCloseDaemonSetDetail = (): void => { setSelectedDaemonSetUID(null); - setSearchParams({ - ...Object.fromEntries( - Array.from(searchParams.entries()).filter( - ([key]) => - ![ - INFRA_MONITORING_K8S_PARAMS_KEYS.DAEMONSET_UID, - INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW, - INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS, - INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS, - INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS, - ].includes(key), - ), - ), - }); + setView(null); + setTracesFilters(null); + setEventsFilters(null); + setLogFilters(null); }; const handleGroupByChange = useCallback( @@ -650,10 +602,6 @@ function K8sDaemonSetsList({ setCurrentPage(1); setGroupBy(groupBy); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify(groupBy), - }); setExpandedRowKeys([]); logEvent(InfraMonitoringEvents.GroupByChanged, { @@ -662,7 +610,7 @@ function K8sDaemonSetsList({ category: InfraMonitoringEvents.DaemonSet, }); }, - [groupByFiltersData, searchParams, setSearchParams], + [groupByFiltersData, setCurrentPage, setGroupBy], ); useEffect(() => { diff --git a/frontend/src/container/InfraMonitoringK8s/DaemonSets/utils.tsx b/frontend/src/container/InfraMonitoringK8s/DaemonSets/utils.tsx index a6d1c87f26e..2a7e18d3e1a 100644 --- a/frontend/src/container/InfraMonitoringK8s/DaemonSets/utils.tsx +++ b/frontend/src/container/InfraMonitoringK8s/DaemonSets/utils.tsx @@ -1,6 +1,5 @@ import { Color } from '@signozhq/design-tokens'; -import { Tag, Tooltip } from 'antd'; -import { TableColumnType as ColumnType } from 'antd'; +import { TableColumnType as ColumnType, Tag, Tooltip } from 'antd'; import { K8sDaemonSetsData, K8sDaemonSetsListPayload, diff --git a/frontend/src/container/InfraMonitoringK8s/Deployments/DeploymentDetails/DeploymentDetails.tsx b/frontend/src/container/InfraMonitoringK8s/Deployments/DeploymentDetails/DeploymentDetails.tsx index fa063316e43..c59200c0ec8 100644 --- a/frontend/src/container/InfraMonitoringK8s/Deployments/DeploymentDetails/DeploymentDetails.tsx +++ b/frontend/src/container/InfraMonitoringK8s/Deployments/DeploymentDetails/DeploymentDetails.tsx @@ -2,7 +2,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; // eslint-disable-next-line no-restricted-imports import { useSelector } from 'react-redux'; -import { useSearchParams } from 'react-router-dom-v5-compat'; import { Color, Spacing } from '@signozhq/design-tokens'; import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd'; import type { RadioChangeEvent } from 'antd/lib'; @@ -16,15 +15,15 @@ import { initialQueryState, } from 'constants/queryBuilder'; import ROUTES from 'constants/routes'; -import { - filterDuplicateFilters, - getFiltersFromParams, -} from 'container/InfraMonitoringK8s/commonUtils'; -import { - INFRA_MONITORING_K8S_PARAMS_KEYS, - K8sCategory, -} from 'container/InfraMonitoringK8s/constants'; +import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils'; +import { K8sCategory } from 'container/InfraMonitoringK8s/constants'; import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/utils'; +import { + useInfraMonitoringEventsFilters, + useInfraMonitoringLogFilters, + useInfraMonitoringTracesFilters, + useInfraMonitoringView, +} from 'container/InfraMonitoringK8s/hooks'; import { CustomTimeType, Time, @@ -97,23 +96,21 @@ function DeploymentDetails({ : (selectedTime as Time), ); - const [searchParams, setSearchParams] = useSearchParams(); - const [selectedView, setSelectedView] = useState(() => { - const view = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW); - if (view) { - return view as VIEWS; - } - return VIEWS.METRICS; - }); + const [selectedView, setSelectedView] = useInfraMonitoringView(); + const [logFiltersParam, setLogFiltersParam] = useInfraMonitoringLogFilters(); + const [ + tracesFiltersParam, + setTracesFiltersParam, + ] = useInfraMonitoringTracesFilters(); + const [ + eventsFiltersParam, + setEventsFiltersParam, + ] = useInfraMonitoringEventsFilters(); const isDarkMode = useIsDarkMode(); const initialFilters = useMemo(() => { - const urlView = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW); - const queryKey = - urlView === VIEW_TYPES.LOGS - ? INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS - : INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS; - const filters = getFiltersFromParams(searchParams, queryKey); + const filters = + selectedView === VIEW_TYPES.LOGS ? logFiltersParam : tracesFiltersParam; if (filters) { return filters; } @@ -147,16 +144,14 @@ function DeploymentDetails({ }, [ deployment?.meta.k8s_deployment_name, deployment?.meta.k8s_namespace_name, - searchParams, + selectedView, + logFiltersParam, + tracesFiltersParam, ]); const initialEventsFilters = useMemo(() => { - const filters = getFiltersFromParams( - searchParams, - INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS, - ); - if (filters) { - return filters; + if (eventsFiltersParam) { + return eventsFiltersParam; } return { op: 'AND', @@ -185,7 +180,7 @@ function DeploymentDetails({ }, ], }; - }, [deployment?.meta.k8s_deployment_name, searchParams]); + }, [deployment?.meta.k8s_deployment_name, eventsFiltersParam]); const [logAndTracesFilters, setLogAndTracesFilters] = useState< IBuilderQuery['filters'] @@ -226,13 +221,9 @@ function DeploymentDetails({ const handleTabChange = (e: RadioChangeEvent): void => { setSelectedView(e.target.value); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: e.target.value, - [INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(null), - [INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(null), - [INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(null), - }); + setLogFiltersParam(null); + setTracesFiltersParam(null); + setEventsFiltersParam(null); logEvent(InfraMonitoringEvents.TabChanged, { entity: InfraMonitoringEvents.K8sEntity, page: InfraMonitoringEvents.DetailedPage, @@ -309,13 +300,8 @@ function DeploymentDetails({ ), }; - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify( - updatedFilters, - ), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view, - }); + setLogFiltersParam(updatedFilters); + setSelectedView(view); return updatedFilters; }); @@ -354,13 +340,8 @@ function DeploymentDetails({ ), }; - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify( - updatedFilters, - ), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view, - }); + setTracesFiltersParam(updatedFilters); + setSelectedView(view); return updatedFilters; }); @@ -403,13 +384,8 @@ function DeploymentDetails({ ), }; - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify( - updatedFilters, - ), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view, - }); + setEventsFiltersParam(updatedFilters); + setSelectedView(view); return updatedFilters; }); diff --git a/frontend/src/container/InfraMonitoringK8s/Deployments/K8sDeploymentsList.tsx b/frontend/src/container/InfraMonitoringK8s/Deployments/K8sDeploymentsList.tsx index 8a7fa368f8f..a96d3dfe389 100644 --- a/frontend/src/container/InfraMonitoringK8s/Deployments/K8sDeploymentsList.tsx +++ b/frontend/src/container/InfraMonitoringK8s/Deployments/K8sDeploymentsList.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; // eslint-disable-next-line no-restricted-imports -import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly -import { useSearchParams } from 'react-router-dom-v5-compat'; +import { useSelector } from 'react-redux'; import { LoadingOutlined } from '@ant-design/icons'; import { Button, @@ -17,25 +16,34 @@ import logEvent from 'api/common/logEvent'; import { K8sDeploymentsListPayload } from 'api/infraMonitoring/getK8sDeploymentsList'; import classNames from 'classnames'; import { InfraMonitoringEvents } from 'constants/events'; +import { FeatureKeys } from 'constants/features'; import { useGetK8sDeploymentsList } from 'hooks/infraMonitoring/useGetK8sDeploymentsList'; import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations'; import { ChevronDown, ChevronRight } from 'lucide-react'; +import { useAppContext } from 'providers/App/App'; import { AppState } from 'store/reducers'; import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; import { GlobalReducer } from 'types/reducer/globalTime'; import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app'; import { openInNewTab } from 'utils/navigation'; -import { FeatureKeys } from '../../../constants/features'; -import { useAppContext } from '../../../providers/App/App'; -import { getOrderByFromParams } from '../commonUtils'; import { GetK8sEntityToAggregateAttribute, INFRA_MONITORING_K8S_PARAMS_KEYS, K8sCategory, } from '../constants'; +import { + useInfraMonitoringCurrentPage, + useInfraMonitoringDeploymentUID, + useInfraMonitoringEventsFilters, + useInfraMonitoringGroupBy, + useInfraMonitoringLogFilters, + useInfraMonitoringOrderBy, + useInfraMonitoringTracesFilters, + useInfraMonitoringView, +} from '../hooks'; import K8sHeader from '../K8sHeader'; import LoadingContainer from '../LoadingContainer'; import { usePageSize } from '../utils'; @@ -64,55 +72,26 @@ function K8sDeploymentsList({ (state) => state.globalTime, ); - const [searchParams, setSearchParams] = useSearchParams(); - - const [currentPage, setCurrentPage] = useState(() => { - const page = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE); - if (page) { - return parseInt(page, 10); - } - return 1; - }); + const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage(); const [filtersInitialised, setFiltersInitialised] = useState(false); - useEffect(() => { - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE]: currentPage.toString(), - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentPage]); - const [expandedRowKeys, setExpandedRowKeys] = useState([]); - const [orderBy, setOrderBy] = useState<{ - columnName: string; - order: 'asc' | 'desc'; - } | null>(() => getOrderByFromParams(searchParams, true)); + const [orderBy, setOrderBy] = useInfraMonitoringOrderBy(); - const [selectedDeploymentUID, setselectedDeploymentUID] = useState< - string | null - >(() => { - const deploymentUID = searchParams.get( - INFRA_MONITORING_K8S_PARAMS_KEYS.DEPLOYMENT_UID, - ); - if (deploymentUID) { - return deploymentUID; - } - return null; - }); + const [ + selectedDeploymentUID, + setselectedDeploymentUID, + ] = useInfraMonitoringDeploymentUID(); const { pageSize, setPageSize } = usePageSize(K8sCategory.DEPLOYMENTS); - const [groupBy, setGroupBy] = useState(() => { - const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY); - if (groupBy) { - const decoded = decodeURIComponent(groupBy); - const parsed = JSON.parse(decoded); - return parsed as IBuilderQuery['groupBy']; - } - return []; - }); + const [groupBy, setGroupBy] = useInfraMonitoringGroupBy(); + + const [, setView] = useInfraMonitoringView(); + const [, setTracesFilters] = useInfraMonitoringTracesFilters(); + const [, setEventsFilters] = useInfraMonitoringEventsFilters(); + const [, setLogFilters] = useInfraMonitoringLogFilters(); const [ selectedRowData, @@ -139,7 +118,7 @@ function K8sDeploymentsList({ if (quickFiltersLastUpdated !== -1) { setCurrentPage(1); } - }, [quickFiltersLastUpdated]); + }, [quickFiltersLastUpdated, setCurrentPage]); const { featureFlags } = useAppContext(); const dotMetricsEnabled = @@ -192,25 +171,28 @@ function K8sDeploymentsList({ filters, start: Math.floor(minTime / 1000000), end: Math.floor(maxTime / 1000000), - orderBy, + orderBy: orderBy || baseQuery.orderBy, }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [minTime, maxTime, orderBy, selectedRowData, groupBy]); const groupedByRowDataQueryKey = useMemo(() => { + // be careful with what you serialize from selectedRowData + // since it's react node, it could contain circular references + const selectedRowDataKey = JSON.stringify(selectedRowData?.groupedByMeta); if (selectedDeploymentUID) { return [ 'deploymentList', JSON.stringify(queryFilters), JSON.stringify(orderBy), - JSON.stringify(selectedRowData), + selectedRowDataKey, ]; } return [ 'deploymentList', JSON.stringify(queryFilters), JSON.stringify(orderBy), - JSON.stringify(selectedRowData), + selectedRowDataKey, String(minTime), String(maxTime), ]; @@ -269,7 +251,7 @@ function K8sDeploymentsList({ filters: queryFilters, start: Math.floor(minTime / 1000000), end: Math.floor(maxTime / 1000000), - orderBy, + orderBy: orderBy || baseQuery.orderBy, }; if (groupBy.length > 0) { queryPayload.groupBy = groupBy; @@ -381,26 +363,15 @@ function K8sDeploymentsList({ } if ('field' in sorter && sorter.order) { - const currentOrderBy = { + setOrderBy({ columnName: sorter.field as string, order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc', - }; - setOrderBy(currentOrderBy); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify( - currentOrderBy, - ), }); } else { setOrderBy(null); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null), - }); } }, - [searchParams, setSearchParams], + [setCurrentPage, setOrderBy], ); const { handleChangeQueryData } = useQueryOperations({ @@ -465,7 +436,7 @@ function K8sDeploymentsList({ ]); const openDeploymentInNewTab = (record: K8sDeploymentsRowData): void => { - const newParams = new URLSearchParams(searchParams); + const newParams = new URLSearchParams(document.location.search); newParams.set( INFRA_MONITORING_K8S_PARAMS_KEYS.DEPLOYMENT_UID, record.deploymentUID, @@ -489,10 +460,6 @@ function K8sDeploymentsList({ if (groupBy.length === 0) { setSelectedRowData(null); setselectedDeploymentUID(record.deploymentUID); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.DEPLOYMENT_UID]: record.deploymentUID, - }); } else { handleGroupByRowClick(record); } @@ -521,11 +488,6 @@ function K8sDeploymentsList({ setSelectedRowData(null); setGroupBy([]); setOrderBy(null); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify([]), - [INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null), - }); }; const expandedRowRender = (): JSX.Element => ( @@ -622,20 +584,10 @@ function K8sDeploymentsList({ const handleCloseDeploymentDetail = (): void => { setselectedDeploymentUID(null); - setSearchParams({ - ...Object.fromEntries( - Array.from(searchParams.entries()).filter( - ([key]) => - ![ - INFRA_MONITORING_K8S_PARAMS_KEYS.DEPLOYMENT_UID, - INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW, - INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS, - INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS, - INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS, - ].includes(key), - ), - ), - }); + setView(null); + setTracesFilters(null); + setEventsFilters(null); + setLogFilters(null); }; const handleGroupByChange = useCallback( @@ -657,10 +609,6 @@ function K8sDeploymentsList({ // Reset pagination on switching to groupBy setCurrentPage(1); setGroupBy(groupBy); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify(groupBy), - }); setExpandedRowKeys([]); logEvent(InfraMonitoringEvents.GroupByChanged, { @@ -669,7 +617,7 @@ function K8sDeploymentsList({ category: InfraMonitoringEvents.Deployment, }); }, - [groupByFiltersData, searchParams, setSearchParams], + [groupByFiltersData, setCurrentPage, setGroupBy], ); useEffect(() => { diff --git a/frontend/src/container/InfraMonitoringK8s/Deployments/utils.tsx b/frontend/src/container/InfraMonitoringK8s/Deployments/utils.tsx index 1613aa6986f..947a88008e0 100644 --- a/frontend/src/container/InfraMonitoringK8s/Deployments/utils.tsx +++ b/frontend/src/container/InfraMonitoringK8s/Deployments/utils.tsx @@ -1,6 +1,5 @@ import { Color } from '@signozhq/design-tokens'; -import { Tag, Tooltip } from 'antd'; -import { TableColumnType as ColumnType } from 'antd'; +import { TableColumnType as ColumnType, Tag, Tooltip } from 'antd'; import { K8sDeploymentsData, K8sDeploymentsListPayload, diff --git a/frontend/src/container/InfraMonitoringK8s/EntityDetailsUtils/EntityEvents/entityEvents.styles.scss b/frontend/src/container/InfraMonitoringK8s/EntityDetailsUtils/EntityEvents/entityEvents.styles.scss index 60fe9c3efa1..fa253f2abb3 100644 --- a/frontend/src/container/InfraMonitoringK8s/EntityDetailsUtils/EntityEvents/entityEvents.styles.scss +++ b/frontend/src/container/InfraMonitoringK8s/EntityDetailsUtils/EntityEvents/entityEvents.styles.scss @@ -147,9 +147,11 @@ .ant-table-cell:nth-child(n + 3) { padding-right: 24px; } + .column-header-right { text-align: right; } + .ant-table-tbody > tr > td { border-bottom: none; } diff --git a/frontend/src/container/InfraMonitoringK8s/EntityDetailsUtils/EntityTraces/entityTraces.styles.scss b/frontend/src/container/InfraMonitoringK8s/EntityDetailsUtils/EntityTraces/entityTraces.styles.scss index 9c90114edc2..830968367c5 100644 --- a/frontend/src/container/InfraMonitoringK8s/EntityDetailsUtils/EntityTraces/entityTraces.styles.scss +++ b/frontend/src/container/InfraMonitoringK8s/EntityDetailsUtils/EntityTraces/entityTraces.styles.scss @@ -121,9 +121,11 @@ .ant-table-cell:nth-child(n + 3) { padding-right: 24px; } + .column-header-right { text-align: right; } + .ant-table-tbody > tr > td { border-bottom: none; } diff --git a/frontend/src/container/InfraMonitoringK8s/EntityDetailsUtils/entityDetails.styles.scss b/frontend/src/container/InfraMonitoringK8s/EntityDetailsUtils/entityDetails.styles.scss index ff91dffd3d9..1319b5f559d 100644 --- a/frontend/src/container/InfraMonitoringK8s/EntityDetailsUtils/entityDetails.styles.scss +++ b/frontend/src/container/InfraMonitoringK8s/EntityDetailsUtils/entityDetails.styles.scss @@ -102,6 +102,7 @@ .progress-container { width: 158px; + .ant-progress { margin: 0; @@ -185,6 +186,7 @@ box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); } } + .ant-drawer-close { padding: 0px; } @@ -369,9 +371,11 @@ .ant-table-cell:nth-child(n + 3) { padding-right: 24px; } + .column-header-right { text-align: right; } + .ant-table-tbody > tr > td { border-bottom: none; } diff --git a/frontend/src/container/InfraMonitoringK8s/InfraMonitoringK8s.tsx b/frontend/src/container/InfraMonitoringK8s/InfraMonitoringK8s.tsx index 613ab32dcaa..a08c0a48773 100644 --- a/frontend/src/container/InfraMonitoringK8s/InfraMonitoringK8s.tsx +++ b/frontend/src/container/InfraMonitoringK8s/InfraMonitoringK8s.tsx @@ -1,5 +1,4 @@ import { useState } from 'react'; -import { useSearchParams } from 'react-router-dom-v5-compat'; import { VerticalAlignTopOutlined } from '@ant-design/icons'; import * as Sentry from '@sentry/react'; import type { CollapseProps } from 'antd'; @@ -37,11 +36,16 @@ import { GetPodsQuickFiltersConfig, GetStatefulsetsQuickFiltersConfig, GetVolumesQuickFiltersConfig, - INFRA_MONITORING_K8S_PARAMS_KEYS, K8sCategories, } from './constants'; import K8sDaemonSetsList from './DaemonSets/K8sDaemonSetsList'; import K8sDeploymentsList from './Deployments/K8sDeploymentsList'; +import { + useInfraMonitoringCategory, + useInfraMonitoringFilters, + useInfraMonitoringGroupBy, + useInfraMonitoringOrderBy, +} from './hooks'; import K8sJobsList from './Jobs/K8sJobsList'; import K8sNamespacesList from './Namespaces/K8sNamespacesList'; import K8sNodesList from './Nodes/K8sNodesList'; @@ -54,14 +58,11 @@ import './InfraMonitoringK8s.styles.scss'; export default function InfraMonitoringK8s(): JSX.Element { const [showFilters, setShowFilters] = useState(true); - const [searchParams, setSearchParams] = useSearchParams(); - const [selectedCategory, setSelectedCategory] = useState(() => { - const category = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.CATEGORY); - if (category) { - return category as keyof typeof K8sCategories; - } - return K8sCategories.PODS; - }); + const [selectedCategory, setSelectedCategory] = useInfraMonitoringCategory(); + const [, setFilters] = useInfraMonitoringFilters(); + const [, setGroupBy] = useInfraMonitoringGroupBy(); + const [, setOrderBy] = useInfraMonitoringOrderBy(); + const [quickFiltersLastUpdated, setQuickFiltersLastUpdated] = useState(-1); const { currentQuery } = useQueryBuilder(); @@ -86,12 +87,7 @@ export default function InfraMonitoringK8s(): JSX.Element { // in infra monitoring k8s, we are using only one query, hence updating the 0th index of queryData handleChangeQueryData('filters', query.builder.queryData[0].filters); setQuickFiltersLastUpdated(Date.now()); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS]: JSON.stringify( - query.builder.queryData[0].filters, - ), - }); + setFilters(JSON.stringify(query.builder.queryData[0].filters)); logEvent(InfraMonitoringEvents.FilterApplied, { entity: InfraMonitoringEvents.K8sEntity, @@ -317,10 +313,10 @@ export default function InfraMonitoringK8s(): JSX.Element { const handleCategoryChange = (key: string | string[]): void => { if (Array.isArray(key) && key.length > 0) { setSelectedCategory(key[0] as string); - setSearchParams({ - [INFRA_MONITORING_K8S_PARAMS_KEYS.CATEGORY]: key[0] as string, - }); // Reset filters + setFilters(null); + setOrderBy(null); + setGroupBy(null); handleChangeQueryData('filters', { items: [], op: 'and' }); } }; diff --git a/frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/JobDetails.tsx b/frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/JobDetails.tsx index 36830b0544a..cf44a0c5b0c 100644 --- a/frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/JobDetails.tsx +++ b/frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/JobDetails.tsx @@ -2,7 +2,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; // eslint-disable-next-line no-restricted-imports import { useSelector } from 'react-redux'; -import { useSearchParams } from 'react-router-dom-v5-compat'; import { Color, Spacing } from '@signozhq/design-tokens'; import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd'; import type { RadioChangeEvent } from 'antd/lib'; @@ -15,11 +14,14 @@ import { initialQueryState, } from 'constants/queryBuilder'; import ROUTES from 'constants/routes'; -import { getFiltersFromParams } from 'container/InfraMonitoringK8s/commonUtils'; +import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils'; +import { K8sCategory } from 'container/InfraMonitoringK8s/constants'; import { - INFRA_MONITORING_K8S_PARAMS_KEYS, - K8sCategory, -} from 'container/InfraMonitoringK8s/constants'; + useInfraMonitoringEventsFilters, + useInfraMonitoringLogFilters, + useInfraMonitoringTracesFilters, + useInfraMonitoringView, +} from 'container/InfraMonitoringK8s/hooks'; import { CustomTimeType, Time, @@ -90,23 +92,21 @@ function JobDetails({ : (selectedTime as Time), ); - const [searchParams, setSearchParams] = useSearchParams(); - const [selectedView, setSelectedView] = useState(() => { - const view = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW); - if (view) { - return view as VIEWS; - } - return VIEWS.METRICS; - }); + const [selectedView, setSelectedView] = useInfraMonitoringView(); + const [logFiltersParam, setLogFiltersParam] = useInfraMonitoringLogFilters(); + const [ + tracesFiltersParam, + setTracesFiltersParam, + ] = useInfraMonitoringTracesFilters(); + const [ + eventsFiltersParam, + setEventsFiltersParam, + ] = useInfraMonitoringEventsFilters(); const isDarkMode = useIsDarkMode(); const initialFilters = useMemo(() => { - const urlView = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW); - const queryKey = - urlView === VIEW_TYPES.LOGS - ? INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS - : INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS; - const filters = getFiltersFromParams(searchParams, queryKey); + const filters = + selectedView === VIEW_TYPES.LOGS ? logFiltersParam : tracesFiltersParam; if (filters) { return filters; } @@ -137,15 +137,17 @@ function JobDetails({ }, ], }; - }, [job?.meta.k8s_job_name, job?.meta.k8s_namespace_name, searchParams]); + }, [ + job?.meta.k8s_job_name, + job?.meta.k8s_namespace_name, + selectedView, + logFiltersParam, + tracesFiltersParam, + ]); const initialEventsFilters = useMemo(() => { - const filters = getFiltersFromParams( - searchParams, - INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS, - ); - if (filters) { - return filters; + if (eventsFiltersParam) { + return eventsFiltersParam; } return { op: 'AND', @@ -174,7 +176,7 @@ function JobDetails({ }, ], }; - }, [job?.meta.k8s_job_name, searchParams]); + }, [job?.meta.k8s_job_name, eventsFiltersParam]); const [logAndTracesFilters, setLogAndTracesFilters] = useState< IBuilderQuery['filters'] @@ -215,13 +217,9 @@ function JobDetails({ const handleTabChange = (e: RadioChangeEvent): void => { setSelectedView(e.target.value); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: e.target.value, - [INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(null), - [INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(null), - [INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(null), - }); + setLogFiltersParam(null); + setTracesFiltersParam(null); + setEventsFiltersParam(null); logEvent(InfraMonitoringEvents.TabChanged, { entity: InfraMonitoringEvents.K8sEntity, page: InfraMonitoringEvents.DetailedPage, @@ -288,20 +286,17 @@ function JobDetails({ const updatedFilters = { op: 'AND', - items: [ - ...(primaryFilters || []), - ...(newFilters || []), - ...(paginationFilter ? [paginationFilter] : []), - ].filter((item): item is TagFilterItem => item !== undefined), + items: filterDuplicateFilters( + [ + ...(primaryFilters || []), + ...(newFilters || []), + ...(paginationFilter ? [paginationFilter] : []), + ].filter((item): item is TagFilterItem => item !== undefined), + ), }; - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify( - updatedFilters, - ), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view, - }); + setLogFiltersParam(updatedFilters); + setSelectedView(view); return updatedFilters; }); @@ -330,21 +325,18 @@ function JobDetails({ const updatedFilters = { op: 'AND', - items: [ - ...(primaryFilters || []), - ...(value?.items?.filter( - (item) => item.key?.key !== QUERY_KEYS.K8S_JOB_NAME, - ) || []), - ].filter((item): item is TagFilterItem => item !== undefined), + items: filterDuplicateFilters( + [ + ...(primaryFilters || []), + ...(value?.items?.filter( + (item) => item.key?.key !== QUERY_KEYS.K8S_JOB_NAME, + ) || []), + ].filter((item): item is TagFilterItem => item !== undefined), + ), }; - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify( - updatedFilters, - ), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view, - }); + setTracesFiltersParam(updatedFilters); + setSelectedView(view); return updatedFilters; }); @@ -385,13 +377,8 @@ function JobDetails({ ].filter((item): item is TagFilterItem => item !== undefined), }; - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify( - updatedFilters, - ), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view, - }); + setEventsFiltersParam(updatedFilters); + setSelectedView(view); return updatedFilters; }); diff --git a/frontend/src/container/InfraMonitoringK8s/Jobs/K8sJobsList.tsx b/frontend/src/container/InfraMonitoringK8s/Jobs/K8sJobsList.tsx index 05780748c1c..1909f625891 100644 --- a/frontend/src/container/InfraMonitoringK8s/Jobs/K8sJobsList.tsx +++ b/frontend/src/container/InfraMonitoringK8s/Jobs/K8sJobsList.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; // eslint-disable-next-line no-restricted-imports -import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly -import { useSearchParams } from 'react-router-dom-v5-compat'; +import { useSelector } from 'react-redux'; import { LoadingOutlined } from '@ant-design/icons'; import { Button, @@ -17,25 +16,34 @@ import logEvent from 'api/common/logEvent'; import { K8sJobsListPayload } from 'api/infraMonitoring/getK8sJobsList'; import classNames from 'classnames'; import { InfraMonitoringEvents } from 'constants/events'; +import { FeatureKeys } from 'constants/features'; import { useGetK8sJobsList } from 'hooks/infraMonitoring/useGetK8sJobsList'; import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations'; import { ChevronDown, ChevronRight } from 'lucide-react'; +import { useAppContext } from 'providers/App/App'; import { AppState } from 'store/reducers'; import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; import { GlobalReducer } from 'types/reducer/globalTime'; import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app'; import { openInNewTab } from 'utils/navigation'; -import { FeatureKeys } from '../../../constants/features'; -import { useAppContext } from '../../../providers/App/App'; -import { getOrderByFromParams } from '../commonUtils'; import { GetK8sEntityToAggregateAttribute, INFRA_MONITORING_K8S_PARAMS_KEYS, K8sCategory, } from '../constants'; +import { + useInfraMonitoringCurrentPage, + useInfraMonitoringEventsFilters, + useInfraMonitoringGroupBy, + useInfraMonitoringJobUID, + useInfraMonitoringLogFilters, + useInfraMonitoringOrderBy, + useInfraMonitoringTracesFilters, + useInfraMonitoringView, +} from '../hooks'; import K8sHeader from '../K8sHeader'; import LoadingContainer from '../LoadingContainer'; import { usePageSize } from '../utils'; @@ -63,51 +71,24 @@ function K8sJobsList({ const { maxTime, minTime } = useSelector( (state) => state.globalTime, ); - const [searchParams, setSearchParams] = useSearchParams(); - const [currentPage, setCurrentPage] = useState(() => { - const page = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE); - if (page) { - return parseInt(page, 10); - } - return 1; - }); + const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage(); const [filtersInitialised, setFiltersInitialised] = useState(false); - useEffect(() => { - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE]: currentPage.toString(), - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentPage]); - const [expandedRowKeys, setExpandedRowKeys] = useState([]); - const [orderBy, setOrderBy] = useState<{ - columnName: string; - order: 'asc' | 'desc'; - } | null>(() => getOrderByFromParams(searchParams, true)); + const [orderBy, setOrderBy] = useInfraMonitoringOrderBy(); - const [selectedJobUID, setselectedJobUID] = useState(() => { - const jobUID = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.JOB_UID); - if (jobUID) { - return jobUID; - } - return null; - }); + const [selectedJobUID, setselectedJobUID] = useInfraMonitoringJobUID(); const { pageSize, setPageSize } = usePageSize(K8sCategory.JOBS); - const [groupBy, setGroupBy] = useState(() => { - const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY); - if (groupBy) { - const decoded = decodeURIComponent(groupBy); - const parsed = JSON.parse(decoded); - return parsed as IBuilderQuery['groupBy']; - } - return []; - }); + const [groupBy, setGroupBy] = useInfraMonitoringGroupBy(); + + const [, setView] = useInfraMonitoringView(); + const [, setTracesFilters] = useInfraMonitoringTracesFilters(); + const [, setEventsFilters] = useInfraMonitoringEventsFilters(); + const [, setLogFilters] = useInfraMonitoringLogFilters(); const [selectedRowData, setSelectedRowData] = useState( null, @@ -133,7 +114,7 @@ function K8sJobsList({ if (quickFiltersLastUpdated !== -1) { setCurrentPage(1); } - }, [quickFiltersLastUpdated]); + }, [quickFiltersLastUpdated, setCurrentPage]); const { featureFlags } = useAppContext(); const dotMetricsEnabled = @@ -186,25 +167,28 @@ function K8sJobsList({ filters, start: Math.floor(minTime / 1000000), end: Math.floor(maxTime / 1000000), - orderBy, + orderBy: orderBy || baseQuery.orderBy, }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [minTime, maxTime, orderBy, selectedRowData, groupBy]); const groupedByRowDataQueryKey = useMemo(() => { + // be careful with what you serialize from selectedRowData + // since it's react node, it could contain circular references + const selectedRowDataKey = JSON.stringify(selectedRowData?.groupedByMeta); if (selectedJobUID) { return [ 'jobList', JSON.stringify(queryFilters), JSON.stringify(orderBy), - JSON.stringify(selectedRowData), + selectedRowDataKey, ]; } return [ 'jobList', JSON.stringify(queryFilters), JSON.stringify(orderBy), - JSON.stringify(selectedRowData), + selectedRowDataKey, String(minTime), String(maxTime), ]; @@ -256,7 +240,7 @@ function K8sJobsList({ filters: queryFilters, start: Math.floor(minTime / 1000000), end: Math.floor(maxTime / 1000000), - orderBy, + orderBy: orderBy || baseQuery.orderBy, }; if (groupBy.length > 0) { queryPayload.groupBy = groupBy; @@ -362,26 +346,15 @@ function K8sJobsList({ } if ('field' in sorter && sorter.order) { - const currentOrderBy = { + setOrderBy({ columnName: sorter.field as string, order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc', - }; - setOrderBy(currentOrderBy); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify( - currentOrderBy, - ), }); } else { setOrderBy(null); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null), - }); } }, - [searchParams, setSearchParams], + [setCurrentPage, setOrderBy], ); const { handleChangeQueryData } = useQueryOperations({ @@ -430,7 +403,7 @@ function K8sJobsList({ }, [selectedJobUID, groupBy.length, jobsData, nestedJobsData]); const openJobInNewTab = (record: K8sJobsRowData): void => { - const newParams = new URLSearchParams(searchParams); + const newParams = new URLSearchParams(document.location.search); newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.JOB_UID, record.jobUID); openInNewTab( buildAbsolutePath({ @@ -451,10 +424,6 @@ function K8sJobsList({ if (groupBy.length === 0) { setSelectedRowData(null); setselectedJobUID(record.jobUID); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.JOB_UID]: record.jobUID, - }); } else { handleGroupByRowClick(record); } @@ -483,11 +452,6 @@ function K8sJobsList({ setSelectedRowData(null); setGroupBy([]); setOrderBy(null); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify([]), - [INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null), - }); }; const expandedRowRender = (): JSX.Element => ( @@ -584,20 +548,10 @@ function K8sJobsList({ const handleCloseJobDetail = (): void => { setselectedJobUID(null); - setSearchParams({ - ...Object.fromEntries( - Array.from(searchParams.entries()).filter( - ([key]) => - ![ - INFRA_MONITORING_K8S_PARAMS_KEYS.JOB_UID, - INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW, - INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS, - INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS, - INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS, - ].includes(key), - ), - ), - }); + setView(null); + setTracesFilters(null); + setEventsFilters(null); + setLogFilters(null); }; const handleGroupByChange = useCallback( @@ -619,10 +573,6 @@ function K8sJobsList({ setCurrentPage(1); setGroupBy(groupBy); setExpandedRowKeys([]); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify(groupBy), - }); logEvent(InfraMonitoringEvents.GroupByChanged, { entity: InfraMonitoringEvents.K8sEntity, @@ -630,7 +580,7 @@ function K8sJobsList({ category: InfraMonitoringEvents.Job, }); }, - [groupByFiltersData, searchParams, setSearchParams], + [groupByFiltersData, setCurrentPage, setGroupBy], ); useEffect(() => { diff --git a/frontend/src/container/InfraMonitoringK8s/Jobs/utils.tsx b/frontend/src/container/InfraMonitoringK8s/Jobs/utils.tsx index b339198209b..d33795573bc 100644 --- a/frontend/src/container/InfraMonitoringK8s/Jobs/utils.tsx +++ b/frontend/src/container/InfraMonitoringK8s/Jobs/utils.tsx @@ -1,6 +1,5 @@ import { Color } from '@signozhq/design-tokens'; -import { Tag, Tooltip } from 'antd'; -import { TableColumnType as ColumnType } from 'antd'; +import { TableColumnType as ColumnType, Tag, Tooltip } from 'antd'; import { K8sJobsData, K8sJobsListPayload, diff --git a/frontend/src/container/InfraMonitoringK8s/K8sHeader.tsx b/frontend/src/container/InfraMonitoringK8s/K8sHeader.tsx index 257dd8d310a..95cd29d3aa6 100644 --- a/frontend/src/container/InfraMonitoringK8s/K8sHeader.tsx +++ b/frontend/src/container/InfraMonitoringK8s/K8sHeader.tsx @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import { useCallback, useMemo, useState } from 'react'; -import { useSearchParams } from 'react-router-dom-v5-compat'; import { Button, Select } from 'antd'; import { initialQueriesMap } from 'constants/queryBuilder'; import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch'; @@ -10,7 +9,8 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; import { DataSource } from 'types/common/queryBuilder'; -import { INFRA_MONITORING_K8S_PARAMS_KEYS, K8sCategory } from './constants'; +import { K8sCategory } from './constants'; +import { useInfraMonitoringFiltersK8s } from './hooks'; import K8sFiltersSidePanel from './K8sFiltersSidePanel/K8sFiltersSidePanel'; import { IEntityColumn } from './utils'; @@ -50,17 +50,14 @@ function K8sHeader({ showAutoRefresh, }: K8sHeaderProps): JSX.Element { const [isFiltersSidePanelOpen, setIsFiltersSidePanelOpen] = useState(false); - const [searchParams, setSearchParams] = useSearchParams(); + const [urlFilters, setUrlFilters] = useInfraMonitoringFiltersK8s(); const currentQuery = initialQueriesMap[DataSource.METRICS]; const updatedCurrentQuery = useMemo(() => { - const urlFilters = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS); let { filters } = currentQuery.builder.queryData[0]; if (urlFilters) { - const decoded = decodeURIComponent(urlFilters); - const parsed = JSON.parse(decoded); - filters = parsed; + filters = urlFilters; } return { ...currentQuery, @@ -78,19 +75,16 @@ function K8sHeader({ ], }, }; - }, [currentQuery, searchParams]); + }, [currentQuery, urlFilters]); const query = updatedCurrentQuery?.builder?.queryData[0] || null; const handleChangeTagFilters = useCallback( (value: IBuilderQuery['filters']) => { handleFiltersChange(value); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS]: JSON.stringify(value), - }); + setUrlFilters(value || null); }, - [handleFiltersChange, searchParams, setSearchParams], + [handleFiltersChange, setUrlFilters], ); return ( diff --git a/frontend/src/container/InfraMonitoringK8s/Namespaces/K8sNamespacesList.tsx b/frontend/src/container/InfraMonitoringK8s/Namespaces/K8sNamespacesList.tsx index b57695bd0b1..15fadd8a80c 100644 --- a/frontend/src/container/InfraMonitoringK8s/Namespaces/K8sNamespacesList.tsx +++ b/frontend/src/container/InfraMonitoringK8s/Namespaces/K8sNamespacesList.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; // eslint-disable-next-line no-restricted-imports -import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly -import { useSearchParams } from 'react-router-dom-v5-compat'; +import { useSelector } from 'react-redux'; import { LoadingOutlined } from '@ant-design/icons'; import { Button, @@ -16,25 +15,34 @@ import type { SorterResult } from 'antd/es/table/interface'; import logEvent from 'api/common/logEvent'; import { K8sNamespacesListPayload } from 'api/infraMonitoring/getK8sNamespacesList'; import { InfraMonitoringEvents } from 'constants/events'; +import { FeatureKeys } from 'constants/features'; import { useGetK8sNamespacesList } from 'hooks/infraMonitoring/useGetK8sNamespacesList'; import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations'; import { ChevronDown, ChevronRight } from 'lucide-react'; +import { useAppContext } from 'providers/App/App'; import { AppState } from 'store/reducers'; import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; import { GlobalReducer } from 'types/reducer/globalTime'; import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app'; import { openInNewTab } from 'utils/navigation'; -import { FeatureKeys } from '../../../constants/features'; -import { useAppContext } from '../../../providers/App/App'; -import { getOrderByFromParams } from '../commonUtils'; import { GetK8sEntityToAggregateAttribute, INFRA_MONITORING_K8S_PARAMS_KEYS, K8sCategory, } from '../constants'; +import { + useInfraMonitoringCurrentPage, + useInfraMonitoringEventsFilters, + useInfraMonitoringGroupBy, + useInfraMonitoringLogFilters, + useInfraMonitoringNamespaceUID, + useInfraMonitoringOrderBy, + useInfraMonitoringTracesFilters, + useInfraMonitoringView, +} from '../hooks'; import K8sHeader from '../K8sHeader'; import LoadingContainer from '../LoadingContainer'; import { usePageSize } from '../utils'; @@ -64,53 +72,25 @@ function K8sNamespacesList({ ); const [expandedRowKeys, setExpandedRowKeys] = useState([]); - const [searchParams, setSearchParams] = useSearchParams(); - const [currentPage, setCurrentPage] = useState(() => { - const page = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE); - if (page) { - return parseInt(page, 10); - } - return 1; - }); + const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage(); const [filtersInitialised, setFiltersInitialised] = useState(false); - useEffect(() => { - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE]: currentPage.toString(), - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentPage]); - - const [orderBy, setOrderBy] = useState<{ - columnName: string; - order: 'asc' | 'desc'; - } | null>(() => getOrderByFromParams(searchParams, true)); + const [orderBy, setOrderBy] = useInfraMonitoringOrderBy(); - const [selectedNamespaceUID, setselectedNamespaceUID] = useState< - string | null - >(() => { - const namespaceUID = searchParams.get( - INFRA_MONITORING_K8S_PARAMS_KEYS.NAMESPACE_UID, - ); - if (namespaceUID) { - return namespaceUID; - } - return null; - }); + const [ + selectedNamespaceUID, + setselectedNamespaceUID, + ] = useInfraMonitoringNamespaceUID(); const { pageSize, setPageSize } = usePageSize(K8sCategory.NAMESPACES); - const [groupBy, setGroupBy] = useState(() => { - const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY); - if (groupBy) { - const decoded = decodeURIComponent(groupBy); - const parsed = JSON.parse(decoded); - return parsed as IBuilderQuery['groupBy']; - } - return []; - }); + const [groupBy, setGroupBy] = useInfraMonitoringGroupBy(); + + const [, setView] = useInfraMonitoringView(); + const [, setTracesFilters] = useInfraMonitoringTracesFilters(); + const [, setEventsFilters] = useInfraMonitoringEventsFilters(); + const [, setLogFilters] = useInfraMonitoringLogFilters(); const [ selectedRowData, @@ -137,7 +117,7 @@ function K8sNamespacesList({ if (quickFiltersLastUpdated !== -1) { setCurrentPage(1); } - }, [quickFiltersLastUpdated]); + }, [quickFiltersLastUpdated, setCurrentPage]); const { featureFlags } = useAppContext(); const dotMetricsEnabled = @@ -190,25 +170,28 @@ function K8sNamespacesList({ filters, start: Math.floor(minTime / 1000000), end: Math.floor(maxTime / 1000000), - orderBy, + orderBy: orderBy || baseQuery.orderBy, }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [minTime, maxTime, orderBy, selectedRowData, groupBy]); const groupedByRowDataQueryKey = useMemo(() => { + // be careful with what you serialize from selectedRowData + // since it's react node, it could contain circular references + const selectedRowDataKey = JSON.stringify(selectedRowData?.groupedByMeta); if (selectedNamespaceUID) { return [ 'namespaceList', JSON.stringify(queryFilters), JSON.stringify(orderBy), - JSON.stringify(selectedRowData), + selectedRowDataKey, ]; } return [ 'namespaceList', JSON.stringify(queryFilters), JSON.stringify(orderBy), - JSON.stringify(selectedRowData), + selectedRowDataKey, String(minTime), String(maxTime), ]; @@ -267,7 +250,7 @@ function K8sNamespacesList({ filters: queryFilters, start: Math.floor(minTime / 1000000), end: Math.floor(maxTime / 1000000), - orderBy, + orderBy: orderBy || baseQuery.orderBy, }; if (groupBy.length > 0) { queryPayload.groupBy = groupBy; @@ -377,26 +360,15 @@ function K8sNamespacesList({ } if ('field' in sorter && sorter.order) { - const currentOrderBy = { + setOrderBy({ columnName: sorter.field as string, order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc', - }; - setOrderBy(currentOrderBy); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify( - currentOrderBy, - ), }); } else { setOrderBy(null); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null), - }); } }, - [searchParams, setSearchParams], + [setCurrentPage, setOrderBy], ); const { handleChangeQueryData } = useQueryOperations({ @@ -461,7 +433,7 @@ function K8sNamespacesList({ ]); const openNamespaceInNewTab = (record: K8sNamespacesRowData): void => { - const newParams = new URLSearchParams(searchParams); + const newParams = new URLSearchParams(document.location.search); newParams.set( INFRA_MONITORING_K8S_PARAMS_KEYS.NAMESPACE_UID, record.namespaceUID, @@ -485,10 +457,6 @@ function K8sNamespacesList({ if (groupBy.length === 0) { setSelectedRowData(null); setselectedNamespaceUID(record.namespaceUID); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.NAMESPACE_UID]: record.namespaceUID, - }); } else { handleGroupByRowClick(record); } @@ -517,11 +485,6 @@ function K8sNamespacesList({ setSelectedRowData(null); setGroupBy([]); setOrderBy(null); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify([]), - [INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null), - }); }; const expandedRowRender = (): JSX.Element => ( @@ -618,20 +581,10 @@ function K8sNamespacesList({ const handleCloseNamespaceDetail = (): void => { setselectedNamespaceUID(null); - setSearchParams({ - ...Object.fromEntries( - Array.from(searchParams.entries()).filter( - ([key]) => - ![ - INFRA_MONITORING_K8S_PARAMS_KEYS.NAMESPACE_UID, - INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW, - INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS, - INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS, - INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS, - ].includes(key), - ), - ), - }); + setView(null); + setTracesFilters(null); + setEventsFilters(null); + setLogFilters(null); }; const handleGroupByChange = useCallback( @@ -654,10 +607,6 @@ function K8sNamespacesList({ setCurrentPage(1); setGroupBy(groupBy); setExpandedRowKeys([]); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify(groupBy), - }); logEvent(InfraMonitoringEvents.GroupByChanged, { entity: InfraMonitoringEvents.K8sEntity, @@ -665,7 +614,7 @@ function K8sNamespacesList({ category: InfraMonitoringEvents.Namespace, }); }, - [groupByFiltersData, searchParams, setSearchParams], + [groupByFiltersData, setCurrentPage, setGroupBy], ); useEffect(() => { diff --git a/frontend/src/container/InfraMonitoringK8s/Namespaces/NamespaceDetails/NamespaceDetails.styles.scss b/frontend/src/container/InfraMonitoringK8s/Namespaces/NamespaceDetails/NamespaceDetails.styles.scss index caad387e4b4..4df6ba56ec4 100644 --- a/frontend/src/container/InfraMonitoringK8s/Namespaces/NamespaceDetails/NamespaceDetails.styles.scss +++ b/frontend/src/container/InfraMonitoringK8s/Namespaces/NamespaceDetails/NamespaceDetails.styles.scss @@ -101,6 +101,7 @@ .progress-container { width: 158px; + .ant-progress { margin: 0; @@ -184,6 +185,7 @@ box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); } } + .ant-drawer-close { padding: 0px; } diff --git a/frontend/src/container/InfraMonitoringK8s/Namespaces/NamespaceDetails/NamespaceDetails.tsx b/frontend/src/container/InfraMonitoringK8s/Namespaces/NamespaceDetails/NamespaceDetails.tsx index 8dd4eeffd67..3fa319a21a6 100644 --- a/frontend/src/container/InfraMonitoringK8s/Namespaces/NamespaceDetails/NamespaceDetails.tsx +++ b/frontend/src/container/InfraMonitoringK8s/Namespaces/NamespaceDetails/NamespaceDetails.tsx @@ -2,7 +2,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; // eslint-disable-next-line no-restricted-imports import { useSelector } from 'react-redux'; -import { useSearchParams } from 'react-router-dom-v5-compat'; import { Color, Spacing } from '@signozhq/design-tokens'; import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd'; import type { RadioChangeEvent } from 'antd/lib'; @@ -16,12 +15,15 @@ import { initialQueryState, } from 'constants/queryBuilder'; import ROUTES from 'constants/routes'; -import { getFiltersFromParams } from 'container/InfraMonitoringK8s/commonUtils'; -import { - INFRA_MONITORING_K8S_PARAMS_KEYS, - K8sCategory, -} from 'container/InfraMonitoringK8s/constants'; +import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils'; +import { K8sCategory } from 'container/InfraMonitoringK8s/constants'; import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/utils'; +import { + useInfraMonitoringEventsFilters, + useInfraMonitoringLogFilters, + useInfraMonitoringTracesFilters, + useInfraMonitoringView, +} from 'container/InfraMonitoringK8s/hooks'; import { CustomTimeType, Time, @@ -94,23 +96,21 @@ function NamespaceDetails({ : (selectedTime as Time), ); - const [searchParams, setSearchParams] = useSearchParams(); - const [selectedView, setSelectedView] = useState(() => { - const view = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW); - if (view) { - return view as VIEWS; - } - return VIEWS.METRICS; - }); + const [selectedView, setSelectedView] = useInfraMonitoringView(); + const [logFiltersParam, setLogFiltersParam] = useInfraMonitoringLogFilters(); + const [ + tracesFiltersParam, + setTracesFiltersParam, + ] = useInfraMonitoringTracesFilters(); + const [ + eventsFiltersParam, + setEventsFiltersParam, + ] = useInfraMonitoringEventsFilters(); const isDarkMode = useIsDarkMode(); const initialFilters = useMemo(() => { - const urlView = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW); - const queryKey = - urlView === VIEW_TYPES.LOGS - ? INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS - : INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS; - const filters = getFiltersFromParams(searchParams, queryKey); + const filters = + selectedView === VIEW_TYPES.LOGS ? logFiltersParam : tracesFiltersParam; if (filters) { return filters; } @@ -130,15 +130,16 @@ function NamespaceDetails({ }, ], }; - }, [namespace?.namespaceName, searchParams]); + }, [ + namespace?.namespaceName, + selectedView, + logFiltersParam, + tracesFiltersParam, + ]); const initialEventsFilters = useMemo(() => { - const filters = getFiltersFromParams( - searchParams, - INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS, - ); - if (filters) { - return filters; + if (eventsFiltersParam) { + return eventsFiltersParam; } return { op: 'AND', @@ -167,7 +168,7 @@ function NamespaceDetails({ }, ], }; - }, [namespace?.namespaceName, searchParams]); + }, [namespace?.namespaceName, eventsFiltersParam]); const [logAndTracesFilters, setLogAndTracesFilters] = useState< IBuilderQuery['filters'] @@ -208,13 +209,9 @@ function NamespaceDetails({ const handleTabChange = (e: RadioChangeEvent): void => { setSelectedView(e.target.value); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: e.target.value, - [INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(null), - [INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(null), - [INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(null), - }); + setLogFiltersParam(null); + setTracesFiltersParam(null); + setEventsFiltersParam(null); logEvent(InfraMonitoringEvents.TabChanged, { entity: InfraMonitoringEvents.K8sEntity, page: InfraMonitoringEvents.DetailedPage, @@ -281,21 +278,17 @@ function NamespaceDetails({ const updatedFilters = { op: 'AND', - items: [ - ...(primaryFilters || []), - ...(newFilters || []), - ...(paginationFilter ? [paginationFilter] : []), - ].filter((item): item is TagFilterItem => item !== undefined), + items: filterDuplicateFilters( + [ + ...(primaryFilters || []), + ...(newFilters || []), + ...(paginationFilter ? [paginationFilter] : []), + ].filter((item): item is TagFilterItem => item !== undefined), + ), }; - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view, - [INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify( - updatedFilters, - ), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view, - }); + setLogFiltersParam(updatedFilters); + setSelectedView(view); return updatedFilters; }); @@ -324,21 +317,18 @@ function NamespaceDetails({ const updatedFilters = { op: 'AND', - items: [ - ...(primaryFilters || []), - ...(value?.items?.filter( - (item) => item.key?.key !== QUERY_KEYS.K8S_NAMESPACE_NAME, - ) || []), - ].filter((item): item is TagFilterItem => item !== undefined), + items: filterDuplicateFilters( + [ + ...(primaryFilters || []), + ...(value?.items?.filter( + (item) => item.key?.key !== QUERY_KEYS.K8S_NAMESPACE_NAME, + ) || []), + ].filter((item): item is TagFilterItem => item !== undefined), + ), }; - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view, - [INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify( - updatedFilters, - ), - }); + setTracesFiltersParam(updatedFilters); + setSelectedView(view); return updatedFilters; }); @@ -379,13 +369,8 @@ function NamespaceDetails({ ].filter((item): item is TagFilterItem => item !== undefined), }; - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view, - [INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify( - updatedFilters, - ), - }); + setEventsFiltersParam(updatedFilters); + setSelectedView(view); return updatedFilters; }); diff --git a/frontend/src/container/InfraMonitoringK8s/Namespaces/utils.tsx b/frontend/src/container/InfraMonitoringK8s/Namespaces/utils.tsx index 924f5008381..d1de646505e 100644 --- a/frontend/src/container/InfraMonitoringK8s/Namespaces/utils.tsx +++ b/frontend/src/container/InfraMonitoringK8s/Namespaces/utils.tsx @@ -1,6 +1,5 @@ import { Color } from '@signozhq/design-tokens'; -import { Tag } from 'antd'; -import { TableColumnType as ColumnType } from 'antd'; +import { TableColumnType as ColumnType, Tag } from 'antd'; import { K8sNamespacesData, K8sNamespacesListPayload, diff --git a/frontend/src/container/InfraMonitoringK8s/Nodes/K8sNodesList.tsx b/frontend/src/container/InfraMonitoringK8s/Nodes/K8sNodesList.tsx index 377588ed9a7..ec8ee653198 100644 --- a/frontend/src/container/InfraMonitoringK8s/Nodes/K8sNodesList.tsx +++ b/frontend/src/container/InfraMonitoringK8s/Nodes/K8sNodesList.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; // eslint-disable-next-line no-restricted-imports -import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly -import { useSearchParams } from 'react-router-dom-v5-compat'; +import { useSelector } from 'react-redux'; import { LoadingOutlined } from '@ant-design/icons'; import { Button, @@ -16,25 +15,34 @@ import type { SorterResult } from 'antd/es/table/interface'; import logEvent from 'api/common/logEvent'; import { K8sNodesListPayload } from 'api/infraMonitoring/getK8sNodesList'; import { InfraMonitoringEvents } from 'constants/events'; +import { FeatureKeys } from 'constants/features'; import { useGetK8sNodesList } from 'hooks/infraMonitoring/useGetK8sNodesList'; import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations'; import { ChevronDown, ChevronRight } from 'lucide-react'; +import { useAppContext } from 'providers/App/App'; import { AppState } from 'store/reducers'; import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; import { GlobalReducer } from 'types/reducer/globalTime'; import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app'; import { openInNewTab } from 'utils/navigation'; -import { FeatureKeys } from '../../../constants/features'; -import { useAppContext } from '../../../providers/App/App'; -import { getOrderByFromParams } from '../commonUtils'; import { GetK8sEntityToAggregateAttribute, INFRA_MONITORING_K8S_PARAMS_KEYS, K8sCategory, } from '../constants'; +import { + useInfraMonitoringCurrentPage, + useInfraMonitoringEventsFilters, + useInfraMonitoringGroupBy, + useInfraMonitoringLogFilters, + useInfraMonitoringNodeUID, + useInfraMonitoringOrderBy, + useInfraMonitoringTracesFilters, + useInfraMonitoringView, +} from '../hooks'; import K8sHeader from '../K8sHeader'; import LoadingContainer from '../LoadingContainer'; import { usePageSize } from '../utils'; @@ -62,50 +70,24 @@ function K8sNodesList({ const { maxTime, minTime } = useSelector( (state) => state.globalTime, ); - const [searchParams, setSearchParams] = useSearchParams(); - const [currentPage, setCurrentPage] = useState(() => { - const page = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE); - if (page) { - return parseInt(page, 10); - } - return 1; - }); + const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage(); const [filtersInitialised, setFiltersInitialised] = useState(false); - useEffect(() => { - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE]: currentPage.toString(), - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentPage]); - const [expandedRowKeys, setExpandedRowKeys] = useState([]); - const [orderBy, setOrderBy] = useState<{ - columnName: string; - order: 'asc' | 'desc'; - } | null>(() => getOrderByFromParams(searchParams, false)); - - const [selectedNodeUID, setSelectedNodeUID] = useState(() => { - const nodeUID = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.NODE_UID); - if (nodeUID) { - return nodeUID; - } - return null; - }); + const [orderBy, setOrderBy] = useInfraMonitoringOrderBy(); + + const [selectedNodeUID, setSelectedNodeUID] = useInfraMonitoringNodeUID(); const { pageSize, setPageSize } = usePageSize(K8sCategory.NODES); - const [groupBy, setGroupBy] = useState(() => { - const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY); - if (groupBy) { - const decoded = decodeURIComponent(groupBy); - const parsed = JSON.parse(decoded); - return parsed as IBuilderQuery['groupBy']; - } - return []; - }); + const [groupBy, setGroupBy] = useInfraMonitoringGroupBy(); + + // These params are used only for clearing in handleCloseNodeDetail + const [, setView] = useInfraMonitoringView(); + const [, setTracesFilters] = useInfraMonitoringTracesFilters(); + const [, setEventsFilters] = useInfraMonitoringEventsFilters(); + const [, setLogFilters] = useInfraMonitoringLogFilters(); const [selectedRowData, setSelectedRowData] = useState( null, @@ -131,7 +113,7 @@ function K8sNodesList({ if (quickFiltersLastUpdated !== -1) { setCurrentPage(1); } - }, [quickFiltersLastUpdated]); + }, [quickFiltersLastUpdated, setCurrentPage]); const { featureFlags } = useAppContext(); const dotMetricsEnabled = @@ -184,25 +166,28 @@ function K8sNodesList({ filters, start: Math.floor(minTime / 1000000), end: Math.floor(maxTime / 1000000), - orderBy, + orderBy: orderBy || baseQuery.orderBy, }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [minTime, maxTime, orderBy, selectedRowData, groupBy]); const groupedByRowDataQueryKey = useMemo(() => { + // be careful with what you serialize from selectedRowData + // since it's react node, it could contain circular references + const selectedRowDataKey = JSON.stringify(selectedRowData?.groupedByMeta); if (selectedNodeUID) { return [ 'nodeList', JSON.stringify(queryFilters), JSON.stringify(orderBy), - JSON.stringify(selectedRowData), + selectedRowDataKey, ]; } return [ 'nodeList', JSON.stringify(queryFilters), JSON.stringify(orderBy), - JSON.stringify(selectedRowData), + selectedRowDataKey, String(minTime), String(maxTime), ]; @@ -261,7 +246,7 @@ function K8sNodesList({ filters: queryFilters, start: Math.floor(minTime / 1000000), end: Math.floor(maxTime / 1000000), - orderBy, + orderBy: orderBy || baseQuery.orderBy, }; if (groupBy.length > 0) { queryPayload.groupBy = groupBy; @@ -367,26 +352,15 @@ function K8sNodesList({ } if ('field' in sorter && sorter.order) { - const currentOrderBy = { + setOrderBy({ columnName: sorter.field as string, order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc', - }; - setOrderBy(currentOrderBy); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify( - currentOrderBy, - ), }); } else { setOrderBy(null); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null), - }); } }, - [searchParams, setSearchParams], + [setCurrentPage, setOrderBy], ); const { handleChangeQueryData } = useQueryOperations({ @@ -440,7 +414,7 @@ function K8sNodesList({ }, [selectedNodeUID, groupBy.length, nodesData, nestedNodesData]); const openNodeInNewTab = (record: K8sNodesRowData): void => { - const newParams = new URLSearchParams(searchParams); + const newParams = new URLSearchParams(document.location.search); newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.NODE_UID, record.nodeUID); openInNewTab( buildAbsolutePath({ @@ -461,10 +435,6 @@ function K8sNodesList({ if (groupBy.length === 0) { setSelectedRowData(null); setSelectedNodeUID(record.nodeUID); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.NODE_UID]: record.nodeUID, - }); } else { handleGroupByRowClick(record); } @@ -493,11 +463,6 @@ function K8sNodesList({ setSelectedRowData(null); setGroupBy([]); setOrderBy(null); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify([]), - [INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null), - }); }; const expandedRowRender = (): JSX.Element => ( @@ -595,45 +560,31 @@ function K8sNodesList({ const handleCloseNodeDetail = (): void => { setSelectedNodeUID(null); - setSearchParams({ - ...Object.fromEntries( - Array.from(searchParams.entries()).filter( - ([key]) => - ![ - INFRA_MONITORING_K8S_PARAMS_KEYS.NODE_UID, - INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW, - INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS, - INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS, - INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS, - ].includes(key), - ), - ), - }); + setView(null); + setTracesFilters(null); + setEventsFilters(null); + setLogFilters(null); }; const handleGroupByChange = useCallback( (value: IBuilderQuery['groupBy']) => { - const groupBy = []; + const newGroupBy = []; for (let index = 0; index < value.length; index++) { const element = (value[index] as unknown) as string; const key = groupByFiltersData?.payload?.attributeKeys?.find( - (key) => key.key === element, + (k) => k.key === element, ); if (key) { - groupBy.push(key); + newGroupBy.push(key); } } // Reset pagination on switching to groupBy setCurrentPage(1); - setGroupBy(groupBy); + setGroupBy(newGroupBy); setExpandedRowKeys([]); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify(groupBy), - }); logEvent(InfraMonitoringEvents.GroupByChanged, { entity: InfraMonitoringEvents.K8sEntity, @@ -641,7 +592,7 @@ function K8sNodesList({ category: InfraMonitoringEvents.Node, }); }, - [groupByFiltersData, searchParams, setSearchParams], + [groupByFiltersData, setCurrentPage, setGroupBy], ); useEffect(() => { diff --git a/frontend/src/container/InfraMonitoringK8s/Nodes/NodeDetails/NodeDetails.tsx b/frontend/src/container/InfraMonitoringK8s/Nodes/NodeDetails/NodeDetails.tsx index dd622eacead..56b91334eca 100644 --- a/frontend/src/container/InfraMonitoringK8s/Nodes/NodeDetails/NodeDetails.tsx +++ b/frontend/src/container/InfraMonitoringK8s/Nodes/NodeDetails/NodeDetails.tsx @@ -2,7 +2,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; // eslint-disable-next-line no-restricted-imports import { useSelector } from 'react-redux'; -import { useSearchParams } from 'react-router-dom-v5-compat'; import { Color, Spacing } from '@signozhq/design-tokens'; import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd'; import type { RadioChangeEvent } from 'antd/lib'; @@ -16,15 +15,15 @@ import { initialQueryState, } from 'constants/queryBuilder'; import ROUTES from 'constants/routes'; -import { - filterDuplicateFilters, - getFiltersFromParams, -} from 'container/InfraMonitoringK8s/commonUtils'; -import { - INFRA_MONITORING_K8S_PARAMS_KEYS, - K8sCategory, -} from 'container/InfraMonitoringK8s/constants'; +import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils'; +import { K8sCategory } from 'container/InfraMonitoringK8s/constants'; import NodeEvents from 'container/InfraMonitoringK8s/EntityDetailsUtils/EntityEvents'; +import { + useInfraMonitoringEventsFilters, + useInfraMonitoringLogFilters, + useInfraMonitoringTracesFilters, + useInfraMonitoringView, +} from 'container/InfraMonitoringK8s/hooks'; import { CustomTimeType, Time, @@ -94,23 +93,21 @@ function NodeDetails({ : (selectedTime as Time), ); - const [searchParams, setSearchParams] = useSearchParams(); - const [selectedView, setSelectedView] = useState(() => { - const view = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW); - if (view) { - return view as VIEWS; - } - return VIEWS.METRICS; - }); + const [selectedView, setSelectedView] = useInfraMonitoringView(); + const [logFiltersParam, setLogFiltersParam] = useInfraMonitoringLogFilters(); + const [ + tracesFiltersParam, + setTracesFiltersParam, + ] = useInfraMonitoringTracesFilters(); + const [ + eventsFiltersParam, + setEventsFiltersParam, + ] = useInfraMonitoringEventsFilters(); const isDarkMode = useIsDarkMode(); const initialFilters = useMemo(() => { - const urlView = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW); - const queryKey = - urlView === VIEW_TYPES.LOGS - ? INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS - : INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS; - const filters = getFiltersFromParams(searchParams, queryKey); + const filters = + selectedView === VIEW_TYPES.LOGS ? logFiltersParam : tracesFiltersParam; if (filters) { return filters; } @@ -130,15 +127,16 @@ function NodeDetails({ }, ], }; - }, [node?.meta.k8s_node_name, searchParams]); + }, [ + node?.meta.k8s_node_name, + selectedView, + logFiltersParam, + tracesFiltersParam, + ]); const initialEventsFilters = useMemo(() => { - const filters = getFiltersFromParams( - searchParams, - INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS, - ); - if (filters) { - return filters; + if (eventsFiltersParam) { + return eventsFiltersParam; } return { op: 'AND', @@ -167,7 +165,7 @@ function NodeDetails({ }, ], }; - }, [node?.meta.k8s_node_name, searchParams]); + }, [node?.meta.k8s_node_name, eventsFiltersParam]); const [logAndTracesFilters, setLogAndTracesFilters] = useState< IBuilderQuery['filters'] @@ -208,13 +206,9 @@ function NodeDetails({ const handleTabChange = (e: RadioChangeEvent): void => { setSelectedView(e.target.value); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: e.target.value, - [INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(null), - [INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(null), - [INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(null), - }); + setLogFiltersParam(null); + setTracesFiltersParam(null); + setEventsFiltersParam(null); logEvent(InfraMonitoringEvents.TabChanged, { entity: InfraMonitoringEvents.K8sEntity, page: InfraMonitoringEvents.DetailedPage, @@ -290,13 +284,8 @@ function NodeDetails({ ), }; - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify( - updatedFilters, - ), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view, - }); + setLogFiltersParam(updatedFilters); + setSelectedView(view); return updatedFilters; }); @@ -335,13 +324,8 @@ function NodeDetails({ ), }; - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify( - updatedFilters, - ), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view, - }); + setTracesFiltersParam(updatedFilters); + setSelectedView(view); return updatedFilters; }); @@ -382,13 +366,8 @@ function NodeDetails({ ].filter((item): item is TagFilterItem => item !== undefined), }; - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify( - updatedFilters, - ), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view, - }); + setEventsFiltersParam(updatedFilters); + setSelectedView(view); return updatedFilters; }); diff --git a/frontend/src/container/InfraMonitoringK8s/Nodes/utils.tsx b/frontend/src/container/InfraMonitoringK8s/Nodes/utils.tsx index 563c51f0bee..601c7d068ef 100644 --- a/frontend/src/container/InfraMonitoringK8s/Nodes/utils.tsx +++ b/frontend/src/container/InfraMonitoringK8s/Nodes/utils.tsx @@ -1,6 +1,5 @@ import { Color } from '@signozhq/design-tokens'; -import { Tag, Tooltip } from 'antd'; -import { TableColumnType as ColumnType } from 'antd'; +import { TableColumnType as ColumnType, Tag, Tooltip } from 'antd'; import { K8sNodesData, K8sNodesListPayload, diff --git a/frontend/src/container/InfraMonitoringK8s/Pods/K8sPodLists.tsx b/frontend/src/container/InfraMonitoringK8s/Pods/K8sPodLists.tsx index f8a2cd8e8ed..ad544388369 100644 --- a/frontend/src/container/InfraMonitoringK8s/Pods/K8sPodLists.tsx +++ b/frontend/src/container/InfraMonitoringK8s/Pods/K8sPodLists.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; // eslint-disable-next-line no-restricted-imports -import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly -import { useSearchParams } from 'react-router-dom-v5-compat'; +import { useSelector } from 'react-redux'; import { LoadingOutlined } from '@ant-design/icons'; import { Button, @@ -19,25 +18,34 @@ import logEvent from 'api/common/logEvent'; import { K8sPodsListPayload } from 'api/infraMonitoring/getK8sPodsList'; import classNames from 'classnames'; import { InfraMonitoringEvents } from 'constants/events'; +import { FeatureKeys } from 'constants/features'; import { useGetK8sPodsList } from 'hooks/infraMonitoring/useGetK8sPodsList'; import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations'; import { ChevronDown, ChevronRight, CornerDownRight } from 'lucide-react'; +import { useAppContext } from 'providers/App/App'; import { AppState } from 'store/reducers'; import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; import { GlobalReducer } from 'types/reducer/globalTime'; import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app'; import { openInNewTab } from 'utils/navigation'; -import { FeatureKeys } from '../../../constants/features'; -import { useAppContext } from '../../../providers/App/App'; -import { getOrderByFromParams } from '../commonUtils'; import { GetK8sEntityToAggregateAttribute, INFRA_MONITORING_K8S_PARAMS_KEYS, K8sCategory, } from '../constants'; +import { + useInfraMonitoringCurrentPage, + useInfraMonitoringEventsFilters, + useInfraMonitoringGroupBy, + useInfraMonitoringLogFilters, + useInfraMonitoringOrderBy, + useInfraMonitoringPodUID, + useInfraMonitoringTracesFilters, + useInfraMonitoringView, +} from '../hooks'; import K8sHeader from '../K8sHeader'; import LoadingContainer from '../LoadingContainer'; import { @@ -66,24 +74,18 @@ function K8sPodsList({ const { maxTime, minTime } = useSelector( (state) => state.globalTime, ); - const [searchParams, setSearchParams] = useSearchParams(); - const [currentPage, setCurrentPage] = useState(() => { - const page = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE); - if (page) { - return parseInt(page, 10); - } - return 1; - }); - const [filtersInitialised, setFiltersInitialised] = useState(false); + const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage(); + const [groupBy, setGroupBy] = useInfraMonitoringGroupBy(); + const [orderBy, setOrderBy] = useInfraMonitoringOrderBy(); + const [defaultOrderBy] = useState(orderBy); + const [selectedPodUID, setSelectedPodUID] = useInfraMonitoringPodUID(); + const [, setView] = useInfraMonitoringView(); + const [, setTracesFilters] = useInfraMonitoringTracesFilters(); + const [, setEventsFilters] = useInfraMonitoringEventsFilters(); + const [, setLogFilters] = useInfraMonitoringLogFilters(); - useEffect(() => { - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE]: currentPage.toString(), - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentPage]); + const [filtersInitialised, setFiltersInitialised] = useState(false); const [addedColumns, setAddedColumns] = useState([]); @@ -91,16 +93,6 @@ function K8sPodsList({ defaultAvailableColumns, ); - const [groupBy, setGroupBy] = useState(() => { - const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY); - if (groupBy) { - const decoded = decodeURIComponent(groupBy); - const parsed = JSON.parse(decoded); - return parsed as IBuilderQuery['groupBy']; - } - return []; - }); - const [selectedRowData, setSelectedRowData] = useState( null, ); @@ -153,7 +145,7 @@ function K8sPodsList({ if (quickFiltersLastUpdated !== -1) { setCurrentPage(1); } - }, [quickFiltersLastUpdated]); + }, [quickFiltersLastUpdated, setCurrentPage]); useEffect(() => { const addedColumns = JSON.parse(get('k8sPodsAddedColumns') ?? '[]'); @@ -172,19 +164,6 @@ function K8sPodsList({ } }, []); - const [orderBy, setOrderBy] = useState<{ - columnName: string; - order: 'asc' | 'desc'; - } | null>(() => getOrderByFromParams(searchParams, false)); - - const [selectedPodUID, setSelectedPodUID] = useState(() => { - const podUID = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.POD_UID); - if (podUID) { - return podUID; - } - return null; - }); - const { pageSize, setPageSize } = usePageSize(K8sCategory.PODS); const query = useMemo(() => { @@ -197,7 +176,7 @@ function K8sPodsList({ filters: queryFilters, start: Math.floor(minTime / 1000000), end: Math.floor(maxTime / 1000000), - orderBy, + orderBy: orderBy || baseQuery.orderBy, }; if (groupBy.length > 0) { @@ -295,25 +274,28 @@ function K8sPodsList({ filters, start: Math.floor(minTime / 1000000), end: Math.floor(maxTime / 1000000), - orderBy, + orderBy: orderBy || baseQuery.orderBy, }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [minTime, maxTime, orderBy, selectedRowData]); const groupedByRowDataQueryKey = useMemo(() => { + // be careful with what you serialize from selectedRowData + // since it's react node, it could contain circular references + const selectedRowDataKey = JSON.stringify(selectedRowData?.groupedByMeta); if (selectedPodUID) { return [ 'podList', JSON.stringify(queryFilters), JSON.stringify(orderBy), - JSON.stringify(selectedRowData), + selectedRowDataKey, ]; } return [ 'podList', JSON.stringify(queryFilters), JSON.stringify(orderBy), - JSON.stringify(selectedRowData), + selectedRowDataKey, String(minTime), String(maxTime), ]; @@ -356,10 +338,10 @@ function K8sPodsList({ [groupedByRowData, groupBy], ); - const columns = useMemo(() => getK8sPodsListColumns(addedColumns, groupBy), [ - addedColumns, - groupBy, - ]); + const columns = useMemo( + () => getK8sPodsListColumns(addedColumns, groupBy, defaultOrderBy), + [addedColumns, groupBy, defaultOrderBy], + ); const handleTableChange: TableProps['onChange'] = useCallback( ( @@ -377,26 +359,15 @@ function K8sPodsList({ } if ('field' in sorter && sorter.order) { - const currentOrderBy = { + setOrderBy({ columnName: sorter.field as string, order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc', - }; - setOrderBy(currentOrderBy); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify( - currentOrderBy, - ), }); } else { setOrderBy(null); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null), - }); } }, - [searchParams, setSearchParams], + [setCurrentPage, setOrderBy], ); const { handleChangeQueryData } = useQueryOperations({ @@ -428,28 +399,24 @@ function K8sPodsList({ const handleGroupByChange = useCallback( (value: IBuilderQuery['groupBy']) => { - const groupBy = []; + const newGroupBy = []; for (let index = 0; index < value.length; index++) { const element = (value[index] as unknown) as string; const key = groupByFiltersData?.payload?.attributeKeys?.find( - (key) => key.key === element, + (k) => k.key === element, ); if (key) { - groupBy.push(key); + newGroupBy.push(key); } } // Reset pagination on switching to groupBy setCurrentPage(1); - setGroupBy(groupBy); + setGroupBy(newGroupBy); setExpandedRowKeys([]); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify(groupBy), - }); logEvent(InfraMonitoringEvents.GroupByChanged, { entity: InfraMonitoringEvents.K8sEntity, @@ -457,7 +424,7 @@ function K8sPodsList({ category: InfraMonitoringEvents.Pod, }); }, - [groupByFiltersData, searchParams, setSearchParams], + [groupByFiltersData, setCurrentPage, setGroupBy], ); useEffect(() => { @@ -498,7 +465,7 @@ function K8sPodsList({ }, [selectedRowData, fetchGroupedByRowData]); const openPodInNewTab = (record: K8sPodsRowData): void => { - const newParams = new URLSearchParams(searchParams); + const newParams = new URLSearchParams(document.location.search); newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.POD_UID, record.podUID); openInNewTab( buildAbsolutePath({ @@ -518,10 +485,6 @@ function K8sPodsList({ } if (groupBy.length === 0) { setSelectedPodUID(record.podUID); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.POD_UID]: record.podUID, - }); setSelectedRowData(null); } else { handleGroupByRowClick(record); @@ -536,20 +499,10 @@ function K8sPodsList({ const handleClosePodDetail = (): void => { setSelectedPodUID(null); - setSearchParams({ - ...Object.fromEntries( - Array.from(searchParams.entries()).filter( - ([key]) => - ![ - INFRA_MONITORING_K8S_PARAMS_KEYS.POD_UID, - INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW, - INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS, - INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS, - INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS, - ].includes(key), - ), - ), - }); + setView(null); + setTracesFilters(null); + setEventsFilters(null); + setLogFilters(null); }; const handleAddColumn = useCallback( @@ -588,9 +541,10 @@ function K8sPodsList({ [setAddedColumns, setAvailableColumns], ); - const nestedColumns = useMemo(() => getK8sPodsListColumns(addedColumns, []), [ - addedColumns, - ]); + const nestedColumns = useMemo( + () => getK8sPodsListColumns(addedColumns, [], defaultOrderBy), + [addedColumns, defaultOrderBy], + ); const isGroupedByAttribute = groupBy.length > 0; @@ -607,11 +561,6 @@ function K8sPodsList({ setSelectedRowData(null); setGroupBy([]); setOrderBy(null); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify([]), - [INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null), - }); }; const expandedRowRender = (): JSX.Element => ( diff --git a/frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodDetails.tsx b/frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodDetails.tsx index a4ce5d7fcde..310b23ab52d 100644 --- a/frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodDetails.tsx +++ b/frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodDetails.tsx @@ -2,7 +2,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; // eslint-disable-next-line no-restricted-imports import { useSelector } from 'react-redux'; -import { useSearchParams } from 'react-router-dom-v5-compat'; import { Color, Spacing } from '@signozhq/design-tokens'; import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd'; import type { RadioChangeEvent } from 'antd/lib'; @@ -16,15 +15,15 @@ import { initialQueryState, } from 'constants/queryBuilder'; import ROUTES from 'constants/routes'; -import { - filterDuplicateFilters, - getFiltersFromParams, -} from 'container/InfraMonitoringK8s/commonUtils'; -import { - INFRA_MONITORING_K8S_PARAMS_KEYS, - K8sCategory, -} from 'container/InfraMonitoringK8s/constants'; +import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils'; +import { K8sCategory } from 'container/InfraMonitoringK8s/constants'; import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/utils'; +import { + useInfraMonitoringEventsFilters, + useInfraMonitoringLogFilters, + useInfraMonitoringTracesFilters, + useInfraMonitoringView, +} from 'container/InfraMonitoringK8s/hooks'; import { CustomTimeType, Time, @@ -96,23 +95,21 @@ function PodDetails({ : (selectedTime as Time), ); - const [searchParams, setSearchParams] = useSearchParams(); - const [selectedView, setSelectedView] = useState(() => { - const view = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW); - if (view) { - return view as VIEWS; - } - return VIEWS.METRICS; - }); + const [selectedView, setSelectedView] = useInfraMonitoringView(); + const [logFiltersParam, setLogFiltersParam] = useInfraMonitoringLogFilters(); + const [ + tracesFiltersParam, + setTracesFiltersParam, + ] = useInfraMonitoringTracesFilters(); + const [ + eventsFiltersParam, + setEventsFiltersParam, + ] = useInfraMonitoringEventsFilters(); const isDarkMode = useIsDarkMode(); const initialFilters = useMemo(() => { - const urlView = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW); - const queryKey = - urlView === VIEW_TYPES.LOGS - ? INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS - : INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS; - const filters = getFiltersFromParams(searchParams, queryKey); + const filters = + selectedView === VIEW_TYPES.LOGS ? logFiltersParam : tracesFiltersParam; if (filters) { return filters; } @@ -143,15 +140,17 @@ function PodDetails({ }, ], }; - }, [pod?.meta.k8s_namespace_name, pod?.meta.k8s_pod_name, searchParams]); + }, [ + pod?.meta.k8s_namespace_name, + pod?.meta.k8s_pod_name, + selectedView, + logFiltersParam, + tracesFiltersParam, + ]); const initialEventsFilters = useMemo(() => { - const filters = getFiltersFromParams( - searchParams, - INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS, - ); - if (filters) { - return filters; + if (eventsFiltersParam) { + return eventsFiltersParam; } return { op: 'AND', @@ -180,7 +179,7 @@ function PodDetails({ }, ], }; - }, [pod?.meta.k8s_pod_name, searchParams]); + }, [pod?.meta.k8s_pod_name, eventsFiltersParam]); const [logsAndTracesFilters, setLogsAndTracesFilters] = useState< IBuilderQuery['filters'] @@ -221,13 +220,9 @@ function PodDetails({ const handleTabChange = (e: RadioChangeEvent): void => { setSelectedView(e.target.value); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: e.target.value, - [INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(null), - [INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(null), - [INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(null), - }); + setLogFiltersParam(null); + setTracesFiltersParam(null); + setEventsFiltersParam(null); logEvent(InfraMonitoringEvents.TabChanged, { entity: InfraMonitoringEvents.K8sEntity, page: InfraMonitoringEvents.DetailedPage, @@ -305,13 +300,8 @@ function PodDetails({ ), }; - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify( - updatedFilters, - ), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view, - }); + setLogFiltersParam(updatedFilters); + setSelectedView(view); return updatedFilters; }); @@ -352,13 +342,8 @@ function PodDetails({ ), }; - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify( - updatedFilters, - ), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view, - }); + setTracesFiltersParam(updatedFilters); + setSelectedView(view); return updatedFilters; }); @@ -399,13 +384,8 @@ function PodDetails({ ].filter((item): item is TagFilterItem => item !== undefined), }; - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify( - updatedFilters, - ), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view, - }); + setEventsFiltersParam(updatedFilters); + setSelectedView(view); return updatedFilters; }); diff --git a/frontend/src/container/InfraMonitoringK8s/StatefulSets/K8sStatefulSetsList.tsx b/frontend/src/container/InfraMonitoringK8s/StatefulSets/K8sStatefulSetsList.tsx index fffda816db6..d6120108144 100644 --- a/frontend/src/container/InfraMonitoringK8s/StatefulSets/K8sStatefulSetsList.tsx +++ b/frontend/src/container/InfraMonitoringK8s/StatefulSets/K8sStatefulSetsList.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; // eslint-disable-next-line no-restricted-imports -import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly -import { useSearchParams } from 'react-router-dom-v5-compat'; +import { useSelector } from 'react-redux'; import { LoadingOutlined } from '@ant-design/icons'; import { Button, @@ -17,25 +16,34 @@ import logEvent from 'api/common/logEvent'; import { K8sStatefulSetsListPayload } from 'api/infraMonitoring/getsK8sStatefulSetsList'; import classNames from 'classnames'; import { InfraMonitoringEvents } from 'constants/events'; +import { FeatureKeys } from 'constants/features'; import { useGetK8sStatefulSetsList } from 'hooks/infraMonitoring/useGetK8sStatefulSetsList'; import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations'; import { ChevronDown, ChevronRight } from 'lucide-react'; +import { useAppContext } from 'providers/App/App'; import { AppState } from 'store/reducers'; import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; import { GlobalReducer } from 'types/reducer/globalTime'; import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app'; import { openInNewTab } from 'utils/navigation'; -import { FeatureKeys } from '../../../constants/features'; -import { useAppContext } from '../../../providers/App/App'; -import { getOrderByFromParams } from '../commonUtils'; import { GetK8sEntityToAggregateAttribute, INFRA_MONITORING_K8S_PARAMS_KEYS, K8sCategory, } from '../constants'; +import { + useInfraMonitoringCurrentPage, + useInfraMonitoringEventsFilters, + useInfraMonitoringGroupBy, + useInfraMonitoringLogFilters, + useInfraMonitoringOrderBy, + useInfraMonitoringStatefulSetUID, + useInfraMonitoringTracesFilters, + useInfraMonitoringView, +} from '../hooks'; import K8sHeader from '../K8sHeader'; import LoadingContainer from '../LoadingContainer'; import { usePageSize } from '../utils'; @@ -50,6 +58,7 @@ import { import '../InfraMonitoringK8s.styles.scss'; import './K8sStatefulSetsList.styles.scss'; + function K8sStatefulSetsList({ isFiltersVisible, handleFilterVisibilityChange, @@ -63,55 +72,26 @@ function K8sStatefulSetsList({ (state) => state.globalTime, ); - const [searchParams, setSearchParams] = useSearchParams(); - - const [currentPage, setCurrentPage] = useState(() => { - const page = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE); - if (page) { - return parseInt(page, 10); - } - return 1; - }); + const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage(); const [filtersInitialised, setFiltersInitialised] = useState(false); - useEffect(() => { - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE]: currentPage.toString(), - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentPage]); - const [expandedRowKeys, setExpandedRowKeys] = useState([]); - const [orderBy, setOrderBy] = useState<{ - columnName: string; - order: 'asc' | 'desc'; - } | null>(() => getOrderByFromParams(searchParams, true)); + const [orderBy, setOrderBy] = useInfraMonitoringOrderBy(); - const [selectedStatefulSetUID, setselectedStatefulSetUID] = useState< - string | null - >(() => { - const statefulSetUID = searchParams.get( - INFRA_MONITORING_K8S_PARAMS_KEYS.STATEFULSET_UID, - ); - if (statefulSetUID) { - return statefulSetUID; - } - return null; - }); + const [ + selectedStatefulSetUID, + setselectedStatefulSetUID, + ] = useInfraMonitoringStatefulSetUID(); const { pageSize, setPageSize } = usePageSize(K8sCategory.STATEFULSETS); - const [groupBy, setGroupBy] = useState(() => { - const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY); - if (groupBy) { - const decoded = decodeURIComponent(groupBy); - const parsed = JSON.parse(decoded); - return parsed as IBuilderQuery['groupBy']; - } - return []; - }); + const [groupBy, setGroupBy] = useInfraMonitoringGroupBy(); + + const [, setView] = useInfraMonitoringView(); + const [, setTracesFilters] = useInfraMonitoringTracesFilters(); + const [, setEventsFilters] = useInfraMonitoringEventsFilters(); + const [, setLogFilters] = useInfraMonitoringLogFilters(); const [ selectedRowData, @@ -138,7 +118,7 @@ function K8sStatefulSetsList({ if (quickFiltersLastUpdated !== -1) { setCurrentPage(1); } - }, [quickFiltersLastUpdated]); + }, [quickFiltersLastUpdated, setCurrentPage]); const { featureFlags } = useAppContext(); const dotMetricsEnabled = @@ -191,25 +171,28 @@ function K8sStatefulSetsList({ filters, start: Math.floor(minTime / 1000000), end: Math.floor(maxTime / 1000000), - orderBy, + orderBy: orderBy || baseQuery.orderBy, }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [minTime, maxTime, orderBy, selectedRowData, groupBy]); const groupedByRowDataQueryKey = useMemo(() => { + // be careful with what you serialize from selectedRowData + // since it's react node, it could contain circular references + const selectedRowDataKey = JSON.stringify(selectedRowData?.groupedByMeta); if (selectedStatefulSetUID) { return [ 'statefulSetList', JSON.stringify(queryFilters), JSON.stringify(orderBy), - JSON.stringify(selectedRowData), + selectedRowDataKey, ]; } return [ 'statefulSetList', JSON.stringify(queryFilters), JSON.stringify(orderBy), - JSON.stringify(selectedRowData), + selectedRowDataKey, String(minTime), String(maxTime), ]; @@ -268,7 +251,7 @@ function K8sStatefulSetsList({ filters: queryFilters, start: Math.floor(minTime / 1000000), end: Math.floor(maxTime / 1000000), - orderBy, + orderBy: orderBy || baseQuery.orderBy, }; if (groupBy.length > 0) { queryPayload.groupBy = groupBy; @@ -380,26 +363,15 @@ function K8sStatefulSetsList({ } if ('field' in sorter && sorter.order) { - const currentOrderBy = { + setOrderBy({ columnName: sorter.field as string, order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc', - }; - setOrderBy(currentOrderBy); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify( - currentOrderBy, - ), }); } else { setOrderBy(null); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null), - }); } }, - [searchParams, setSearchParams], + [setCurrentPage, setOrderBy], ); const { handleChangeQueryData } = useQueryOperations({ @@ -462,7 +434,7 @@ function K8sStatefulSetsList({ ]); const openStatefulSetInNewTab = (record: K8sStatefulSetsRowData): void => { - const newParams = new URLSearchParams(searchParams); + const newParams = new URLSearchParams(document.location.search); newParams.set( INFRA_MONITORING_K8S_PARAMS_KEYS.STATEFULSET_UID, record.statefulsetUID, @@ -486,10 +458,6 @@ function K8sStatefulSetsList({ if (groupBy.length === 0) { setSelectedRowData(null); setselectedStatefulSetUID(record.statefulsetUID); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.STATEFULSET_UID]: record.statefulsetUID, - }); } else { handleGroupByRowClick(record); } @@ -518,11 +486,6 @@ function K8sStatefulSetsList({ setSelectedRowData(null); setGroupBy([]); setOrderBy(null); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify([]), - [INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null), - }); }; const expandedRowRender = (): JSX.Element => ( @@ -619,20 +582,10 @@ function K8sStatefulSetsList({ const handleCloseStatefulSetDetail = (): void => { setselectedStatefulSetUID(null); - setSearchParams({ - ...Object.fromEntries( - Array.from(searchParams.entries()).filter( - ([key]) => - ![ - INFRA_MONITORING_K8S_PARAMS_KEYS.STATEFULSET_UID, - INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW, - INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS, - INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS, - INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS, - ].includes(key), - ), - ), - }); + setView(null); + setTracesFilters(null); + setEventsFilters(null); + setLogFilters(null); }; const handleGroupByChange = useCallback( @@ -654,10 +607,6 @@ function K8sStatefulSetsList({ setCurrentPage(1); setGroupBy(groupBy); setExpandedRowKeys([]); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify(groupBy), - }); logEvent(InfraMonitoringEvents.GroupByChanged, { entity: InfraMonitoringEvents.K8sEntity, @@ -665,7 +614,7 @@ function K8sStatefulSetsList({ category: InfraMonitoringEvents.StatefulSet, }); }, - [groupByFiltersData, searchParams, setSearchParams], + [groupByFiltersData, setCurrentPage, setGroupBy], ); useEffect(() => { diff --git a/frontend/src/container/InfraMonitoringK8s/StatefulSets/StatefulSetDetails/StatefulSetDetails.tsx b/frontend/src/container/InfraMonitoringK8s/StatefulSets/StatefulSetDetails/StatefulSetDetails.tsx index 28a4a252baf..cb04b76c78a 100644 --- a/frontend/src/container/InfraMonitoringK8s/StatefulSets/StatefulSetDetails/StatefulSetDetails.tsx +++ b/frontend/src/container/InfraMonitoringK8s/StatefulSets/StatefulSetDetails/StatefulSetDetails.tsx @@ -2,7 +2,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; // eslint-disable-next-line no-restricted-imports import { useSelector } from 'react-redux'; -import { useSearchParams } from 'react-router-dom-v5-compat'; import { Color, Spacing } from '@signozhq/design-tokens'; import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd'; import type { RadioChangeEvent } from 'antd/lib'; @@ -15,16 +14,19 @@ import { initialQueryState, } from 'constants/queryBuilder'; import ROUTES from 'constants/routes'; -import { getFiltersFromParams } from 'container/InfraMonitoringK8s/commonUtils'; -import { - INFRA_MONITORING_K8S_PARAMS_KEYS, - K8sCategory, -} from 'container/InfraMonitoringK8s/constants'; +import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils'; +import { K8sCategory } from 'container/InfraMonitoringK8s/constants'; import EntityEvents from 'container/InfraMonitoringK8s/EntityDetailsUtils/EntityEvents'; import EntityLogs from 'container/InfraMonitoringK8s/EntityDetailsUtils/EntityLogs'; import EntityMetrics from 'container/InfraMonitoringK8s/EntityDetailsUtils/EntityMetrics'; import EntityTraces from 'container/InfraMonitoringK8s/EntityDetailsUtils/EntityTraces'; import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/utils'; +import { + useInfraMonitoringEventsFilters, + useInfraMonitoringLogFilters, + useInfraMonitoringTracesFilters, + useInfraMonitoringView, +} from 'container/InfraMonitoringK8s/hooks'; import { CustomTimeType, Time, @@ -93,23 +95,21 @@ function StatefulSetDetails({ : (selectedTime as Time), ); - const [searchParams, setSearchParams] = useSearchParams(); - const [selectedView, setSelectedView] = useState(() => { - const view = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW); - if (view) { - return view as VIEWS; - } - return VIEWS.METRICS; - }); + const [selectedView, setSelectedView] = useInfraMonitoringView(); + const [logFiltersParam, setLogFiltersParam] = useInfraMonitoringLogFilters(); + const [ + tracesFiltersParam, + setTracesFiltersParam, + ] = useInfraMonitoringTracesFilters(); + const [ + eventsFiltersParam, + setEventsFiltersParam, + ] = useInfraMonitoringEventsFilters(); const isDarkMode = useIsDarkMode(); const initialFilters = useMemo(() => { - const urlView = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW); - const queryKey = - urlView === VIEW_TYPES.LOGS - ? INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS - : INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS; - const filters = getFiltersFromParams(searchParams, queryKey); + const filters = + selectedView === VIEW_TYPES.LOGS ? logFiltersParam : tracesFiltersParam; if (filters) { return filters; } @@ -141,18 +141,16 @@ function StatefulSetDetails({ ], }; }, [ - searchParams, statefulSet?.meta.k8s_statefulset_name, statefulSet?.meta.k8s_namespace_name, + selectedView, + logFiltersParam, + tracesFiltersParam, ]); const initialEventsFilters = useMemo(() => { - const filters = getFiltersFromParams( - searchParams, - INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS, - ); - if (filters) { - return filters; + if (eventsFiltersParam) { + return eventsFiltersParam; } return { op: 'AND', @@ -181,7 +179,7 @@ function StatefulSetDetails({ }, ], }; - }, [searchParams, statefulSet?.meta.k8s_statefulset_name]); + }, [statefulSet?.meta.k8s_statefulset_name, eventsFiltersParam]); const [logAndTracesFilters, setLogAndTracesFilters] = useState< IBuilderQuery['filters'] @@ -222,13 +220,9 @@ function StatefulSetDetails({ const handleTabChange = (e: RadioChangeEvent): void => { setSelectedView(e.target.value); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: e.target.value, - [INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(null), - [INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(null), - [INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify(null), - }); + setLogFiltersParam(null); + setTracesFiltersParam(null); + setEventsFiltersParam(null); logEvent(InfraMonitoringEvents.TabChanged, { entity: InfraMonitoringEvents.K8sEntity, page: InfraMonitoringEvents.DetailedPage, @@ -296,20 +290,17 @@ function StatefulSetDetails({ const updatedFilters = { op: 'AND', - items: [ - ...(primaryFilters || []), - ...(newFilters || []), - ...(paginationFilter ? [paginationFilter] : []), - ].filter((item): item is TagFilterItem => item !== undefined), + items: filterDuplicateFilters( + [ + ...(primaryFilters || []), + ...(newFilters || []), + ...(paginationFilter ? [paginationFilter] : []), + ].filter((item): item is TagFilterItem => item !== undefined), + ), }; - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify( - updatedFilters, - ), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view, - }); + setLogFiltersParam(updatedFilters); + setSelectedView(view); return updatedFilters; }); @@ -338,21 +329,18 @@ function StatefulSetDetails({ const updatedFilters = { op: 'AND', - items: [ - ...(primaryFilters || []), - ...(value?.items?.filter( - (item) => item.key?.key !== QUERY_KEYS.K8S_STATEFUL_SET_NAME, - ) || []), - ].filter((item): item is TagFilterItem => item !== undefined), + items: filterDuplicateFilters( + [ + ...(primaryFilters || []), + ...(value?.items?.filter( + (item) => item.key?.key !== QUERY_KEYS.K8S_STATEFUL_SET_NAME, + ) || []), + ].filter((item): item is TagFilterItem => item !== undefined), + ), }; - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify( - updatedFilters, - ), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view, - }); + setTracesFiltersParam(updatedFilters); + setSelectedView(view); return updatedFilters; }); @@ -393,13 +381,8 @@ function StatefulSetDetails({ ].filter((item): item is TagFilterItem => item !== undefined), }; - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS]: JSON.stringify( - updatedFilters, - ), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view, - }); + setEventsFiltersParam(updatedFilters); + setSelectedView(view); return updatedFilters; }); diff --git a/frontend/src/container/InfraMonitoringK8s/StatefulSets/utils.tsx b/frontend/src/container/InfraMonitoringK8s/StatefulSets/utils.tsx index f500779b003..9129d163d90 100644 --- a/frontend/src/container/InfraMonitoringK8s/StatefulSets/utils.tsx +++ b/frontend/src/container/InfraMonitoringK8s/StatefulSets/utils.tsx @@ -1,6 +1,5 @@ import { Color } from '@signozhq/design-tokens'; -import { Tag, Tooltip } from 'antd'; -import { TableColumnType as ColumnType } from 'antd'; +import { TableColumnType as ColumnType, Tag, Tooltip } from 'antd'; import { K8sStatefulSetsData, K8sStatefulSetsListPayload, diff --git a/frontend/src/container/InfraMonitoringK8s/Volumes/K8sVolumesList.tsx b/frontend/src/container/InfraMonitoringK8s/Volumes/K8sVolumesList.tsx index a9de74321f4..16945b48aaa 100644 --- a/frontend/src/container/InfraMonitoringK8s/Volumes/K8sVolumesList.tsx +++ b/frontend/src/container/InfraMonitoringK8s/Volumes/K8sVolumesList.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; // eslint-disable-next-line no-restricted-imports -import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly -import { useSearchParams } from 'react-router-dom-v5-compat'; +import { useSelector } from 'react-redux'; import { LoadingOutlined } from '@ant-design/icons'; import { Button, @@ -17,25 +16,30 @@ import logEvent from 'api/common/logEvent'; import { K8sVolumesListPayload } from 'api/infraMonitoring/getK8sVolumesList'; import classNames from 'classnames'; import { InfraMonitoringEvents } from 'constants/events'; +import { FeatureKeys } from 'constants/features'; import { useGetK8sVolumesList } from 'hooks/infraMonitoring/useGetK8sVolumesList'; import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations'; import { ChevronDown, ChevronRight } from 'lucide-react'; +import { useAppContext } from 'providers/App/App'; import { AppState } from 'store/reducers'; import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; import { GlobalReducer } from 'types/reducer/globalTime'; import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app'; import { openInNewTab } from 'utils/navigation'; -import { FeatureKeys } from '../../../constants/features'; -import { useAppContext } from '../../../providers/App/App'; -import { getOrderByFromParams } from '../commonUtils'; import { GetK8sEntityToAggregateAttribute, INFRA_MONITORING_K8S_PARAMS_KEYS, K8sCategory, } from '../constants'; +import { + useInfraMonitoringCurrentPage, + useInfraMonitoringGroupBy, + useInfraMonitoringOrderBy, + useInfraMonitoringVolumeUID, +} from '../hooks'; import K8sHeader from '../K8sHeader'; import LoadingContainer from '../LoadingContainer'; import { usePageSize } from '../utils'; @@ -50,6 +54,7 @@ import VolumeDetails from './VolumeDetails'; import '../InfraMonitoringK8s.styles.scss'; import './K8sVolumesList.styles.scss'; + function K8sVolumesList({ isFiltersVisible, handleFilterVisibilityChange, @@ -63,55 +68,21 @@ function K8sVolumesList({ (state) => state.globalTime, ); - const [searchParams, setSearchParams] = useSearchParams(); - - const [currentPage, setCurrentPage] = useState(() => { - const page = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE); - if (page) { - return parseInt(page, 10); - } - return 1; - }); + const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage(); const [filtersInitialised, setFiltersInitialised] = useState(false); - useEffect(() => { - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE]: currentPage.toString(), - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentPage]); - const [expandedRowKeys, setExpandedRowKeys] = useState([]); - const [orderBy, setOrderBy] = useState<{ - columnName: string; - order: 'asc' | 'desc'; - } | null>(() => getOrderByFromParams(searchParams, true)); + const [orderBy, setOrderBy] = useInfraMonitoringOrderBy(); - const [selectedVolumeUID, setselectedVolumeUID] = useState( - () => { - const volumeUID = searchParams.get( - INFRA_MONITORING_K8S_PARAMS_KEYS.VOLUME_UID, - ); - if (volumeUID) { - return volumeUID; - } - return null; - }, - ); + const [ + selectedVolumeUID, + setselectedVolumeUID, + ] = useInfraMonitoringVolumeUID(); const { pageSize, setPageSize } = usePageSize(K8sCategory.VOLUMES); - const [groupBy, setGroupBy] = useState(() => { - const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY); - if (groupBy) { - const decoded = decodeURIComponent(groupBy); - const parsed = JSON.parse(decoded); - return parsed as IBuilderQuery['groupBy']; - } - return []; - }); + const [groupBy, setGroupBy] = useInfraMonitoringGroupBy(); const [ selectedRowData, @@ -138,7 +109,7 @@ function K8sVolumesList({ if (quickFiltersLastUpdated !== -1) { setCurrentPage(1); } - }, [quickFiltersLastUpdated]); + }, [quickFiltersLastUpdated, setCurrentPage]); const { featureFlags } = useAppContext(); const dotMetricsEnabled = @@ -191,7 +162,7 @@ function K8sVolumesList({ filters, start: Math.floor(minTime / 1000000), end: Math.floor(maxTime / 1000000), - orderBy, + orderBy: orderBy || baseQuery.orderBy, }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [minTime, maxTime, orderBy, selectedRowData, groupBy]); @@ -242,7 +213,7 @@ function K8sVolumesList({ filters: queryFilters, start: Math.floor(minTime / 1000000), end: Math.floor(maxTime / 1000000), - orderBy, + orderBy: orderBy || baseQuery.orderBy, }; if (groupBy.length > 0) { queryPayload.groupBy = groupBy; @@ -315,26 +286,15 @@ function K8sVolumesList({ } if ('field' in sorter && sorter.order) { - const currentOrderBy = { + setOrderBy({ columnName: sorter.field as string, order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc', - }; - setOrderBy(currentOrderBy); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify( - currentOrderBy, - ), }); } else { setOrderBy(null); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null), - }); } }, - [searchParams, setSearchParams], + [setCurrentPage, setOrderBy], ); const { handleChangeQueryData } = useQueryOperations({ @@ -392,7 +352,7 @@ function K8sVolumesList({ }, [selectedVolumeUID, volumesData, groupBy.length, nestedVolumesData]); const openVolumeInNewTab = (record: K8sVolumesRowData): void => { - const newParams = new URLSearchParams(searchParams); + const newParams = new URLSearchParams(document.location.search); newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.VOLUME_UID, record.volumeUID); openInNewTab( buildAbsolutePath({ @@ -413,10 +373,6 @@ function K8sVolumesList({ if (groupBy.length === 0) { setSelectedRowData(null); setselectedVolumeUID(record.volumeUID); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.VOLUME_UID]: record.volumeUID, - }); } else { handleGroupByRowClick(record); } @@ -445,11 +401,6 @@ function K8sVolumesList({ setSelectedRowData(null); setGroupBy([]); setOrderBy(null); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify([]), - [INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(null), - }); }; const expandedRowRender = (): JSX.Element => ( @@ -546,13 +497,6 @@ function K8sVolumesList({ const handleCloseVolumeDetail = (): void => { setselectedVolumeUID(null); - setSearchParams({ - ...Object.fromEntries( - Array.from(searchParams.entries()).filter( - ([key]) => key !== INFRA_MONITORING_K8S_PARAMS_KEYS.VOLUME_UID, - ), - ), - }); }; const handleGroupByChange = useCallback( @@ -573,10 +517,6 @@ function K8sVolumesList({ setCurrentPage(1); setGroupBy(groupBy); - setSearchParams({ - ...Object.fromEntries(searchParams.entries()), - [INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify(groupBy), - }); setExpandedRowKeys([]); logEvent(InfraMonitoringEvents.GroupByChanged, { @@ -585,7 +525,7 @@ function K8sVolumesList({ category: InfraMonitoringEvents.Volumes, }); }, - [groupByFiltersData?.payload?.attributeKeys, searchParams, setSearchParams], + [groupByFiltersData?.payload?.attributeKeys, setCurrentPage, setGroupBy], ); useEffect(() => { diff --git a/frontend/src/container/InfraMonitoringK8s/Volumes/utils.tsx b/frontend/src/container/InfraMonitoringK8s/Volumes/utils.tsx index a05c6a8dc79..7beabb14974 100644 --- a/frontend/src/container/InfraMonitoringK8s/Volumes/utils.tsx +++ b/frontend/src/container/InfraMonitoringK8s/Volumes/utils.tsx @@ -1,6 +1,5 @@ import { Color } from '@signozhq/design-tokens'; -import { Tag, Tooltip } from 'antd'; -import { TableColumnType as ColumnType } from 'antd'; +import { TableColumnType as ColumnType, Tag, Tooltip } from 'antd'; import { K8sVolumesData, K8sVolumesListPayload, @@ -75,7 +74,7 @@ export const getK8sVolumesListQuery = (): K8sVolumesListPayload => ({ items: [], op: 'and', }, - orderBy: { columnName: 'cpu', order: 'desc' }, + orderBy: { columnName: 'usage', order: 'desc' }, }); const columnsConfig = [ diff --git a/frontend/src/container/InfraMonitoringK8s/__tests__/Clusters/ClusterDetails/ClusterDetails.test.tsx b/frontend/src/container/InfraMonitoringK8s/__tests__/Clusters/ClusterDetails/ClusterDetails.test.tsx index 8fd033440ff..7ffec37d738 100644 --- a/frontend/src/container/InfraMonitoringK8s/__tests__/Clusters/ClusterDetails/ClusterDetails.test.tsx +++ b/frontend/src/container/InfraMonitoringK8s/__tests__/Clusters/ClusterDetails/ClusterDetails.test.tsx @@ -8,10 +8,13 @@ import { Provider } from 'react-redux'; import { MemoryRouter } from 'react-router-dom'; import { fireEvent, render, screen } from '@testing-library/react'; import ClusterDetails from 'container/InfraMonitoringK8s/Clusters/ClusterDetails/ClusterDetails'; +import { withNuqsTestingAdapter } from 'nuqs/adapters/testing'; import store from 'store'; const queryClient = new QueryClient(); +const Wrapper = withNuqsTestingAdapter({ searchParams: {} }); + describe('ClusterDetails', () => { const mockCluster = { meta: { @@ -22,17 +25,19 @@ describe('ClusterDetails', () => { it('should render modal with relevant metadata', () => { render( - - - - - - - , + + + + + + + + + , ); const clusterNameElements = screen.getAllByText('test-cluster'); @@ -42,17 +47,19 @@ describe('ClusterDetails', () => { it('should render modal with 4 tabs', () => { render( - - - - - - - , + + + + + + + + + , ); const metricsTab = screen.getByText('Metrics'); @@ -70,17 +77,19 @@ describe('ClusterDetails', () => { it('default tab should be metrics', () => { render( - - - - - - - , + + + + + + + + + , ); const metricsTab = screen.getByRole('radio', { name: 'Metrics' }); @@ -89,17 +98,19 @@ describe('ClusterDetails', () => { it('should switch to events tab when events tab is clicked', () => { render( - - - - - - - , + + + + + + + + + , ); const eventsTab = screen.getByRole('radio', { name: 'Events' }); @@ -110,17 +121,19 @@ describe('ClusterDetails', () => { it('should close modal when close button is clicked', () => { render( - - - - - - - , + + + + + + + + + , ); const closeButton = screen.getByRole('button', { name: 'Close' }); diff --git a/frontend/src/container/InfraMonitoringK8s/__tests__/DaemonSets/DaemonSetDetails/DaemonSetDetails.test.tsx b/frontend/src/container/InfraMonitoringK8s/__tests__/DaemonSets/DaemonSetDetails/DaemonSetDetails.test.tsx index 0f89c96e388..868124197cc 100644 --- a/frontend/src/container/InfraMonitoringK8s/__tests__/DaemonSets/DaemonSetDetails/DaemonSetDetails.test.tsx +++ b/frontend/src/container/InfraMonitoringK8s/__tests__/DaemonSets/DaemonSetDetails/DaemonSetDetails.test.tsx @@ -8,10 +8,13 @@ import { Provider } from 'react-redux'; import { MemoryRouter } from 'react-router-dom'; import { fireEvent, render, screen } from '@testing-library/react'; import DaemonSetDetails from 'container/InfraMonitoringK8s/DaemonSets/DaemonSetDetails/DaemonSetDetails'; +import { withNuqsTestingAdapter } from 'nuqs/adapters/testing'; import store from 'store'; const queryClient = new QueryClient(); +const Wrapper = withNuqsTestingAdapter({ searchParams: {} }); + describe('DaemonSetDetails', () => { const mockDaemonSet = { meta: { @@ -24,17 +27,19 @@ describe('DaemonSetDetails', () => { it('should render modal with relevant metadata', () => { render( - - - - - - - , + + + + + + + + + , ); const daemonSetNameElements = screen.getAllByText('test-daemon-set'); @@ -52,17 +57,19 @@ describe('DaemonSetDetails', () => { it('should render modal with 4 tabs', () => { render( - - - - - - - , + + + + + + + + + , ); const metricsTab = screen.getByText('Metrics'); @@ -80,17 +87,19 @@ describe('DaemonSetDetails', () => { it('default tab should be metrics', () => { render( - - - - - - - , + + + + + + + + + , ); const metricsTab = screen.getByRole('radio', { name: 'Metrics' }); @@ -99,17 +108,19 @@ describe('DaemonSetDetails', () => { it('should switch to events tab when events tab is clicked', () => { render( - - - - - - - , + + + + + + + + + , ); const eventsTab = screen.getByRole('radio', { name: 'Events' }); @@ -120,17 +131,19 @@ describe('DaemonSetDetails', () => { it('should close modal when close button is clicked', () => { render( - - - - - - - , + + + + + + + + + , ); const closeButton = screen.getByRole('button', { name: 'Close' }); diff --git a/frontend/src/container/InfraMonitoringK8s/__tests__/Deployments/DeploymentDetails/DeploymentDetails.test.tsx b/frontend/src/container/InfraMonitoringK8s/__tests__/Deployments/DeploymentDetails/DeploymentDetails.test.tsx index 40f139cf2d8..d6fcec81a9b 100644 --- a/frontend/src/container/InfraMonitoringK8s/__tests__/Deployments/DeploymentDetails/DeploymentDetails.test.tsx +++ b/frontend/src/container/InfraMonitoringK8s/__tests__/Deployments/DeploymentDetails/DeploymentDetails.test.tsx @@ -8,10 +8,13 @@ import { Provider } from 'react-redux'; import { MemoryRouter } from 'react-router-dom'; import { fireEvent, render, screen } from '@testing-library/react'; import DeploymentDetails from 'container/InfraMonitoringK8s/Deployments/DeploymentDetails/DeploymentDetails'; +import { withNuqsTestingAdapter } from 'nuqs/adapters/testing'; import store from 'store'; const queryClient = new QueryClient(); +const Wrapper = withNuqsTestingAdapter({ searchParams: {} }); + describe('DeploymentDetails', () => { const mockDeployment = { meta: { @@ -24,17 +27,19 @@ describe('DeploymentDetails', () => { it('should render modal with relevant metadata', () => { render( - - - - - - - , + + + + + + + + + , ); const deploymentNameElements = screen.getAllByText('test-deployment'); @@ -52,17 +57,19 @@ describe('DeploymentDetails', () => { it('should render modal with 4 tabs', () => { render( - - - - - - - , + + + + + + + + + , ); const metricsTab = screen.getByText('Metrics'); @@ -80,17 +87,19 @@ describe('DeploymentDetails', () => { it('default tab should be metrics', () => { render( - - - - - - - , + + + + + + + + + , ); const metricsTab = screen.getByRole('radio', { name: 'Metrics' }); @@ -99,17 +108,19 @@ describe('DeploymentDetails', () => { it('should switch to events tab when events tab is clicked', () => { render( - - - - - - - , + + + + + + + + + , ); const eventsTab = screen.getByRole('radio', { name: 'Events' }); @@ -120,17 +131,19 @@ describe('DeploymentDetails', () => { it('should close modal when close button is clicked', () => { render( - - - - - - - , + + + + + + + + + , ); const closeButton = screen.getByRole('button', { name: 'Close' }); diff --git a/frontend/src/container/InfraMonitoringK8s/__tests__/Jobs/JobDetails/JobDetails.test.tsx b/frontend/src/container/InfraMonitoringK8s/__tests__/Jobs/JobDetails/JobDetails.test.tsx index 37d2370e1cf..e6154134201 100644 --- a/frontend/src/container/InfraMonitoringK8s/__tests__/Jobs/JobDetails/JobDetails.test.tsx +++ b/frontend/src/container/InfraMonitoringK8s/__tests__/Jobs/JobDetails/JobDetails.test.tsx @@ -2,8 +2,18 @@ import setupCommonMocks from '../../commonMocks'; setupCommonMocks(); +import { QueryClient, QueryClientProvider } from 'react-query'; +// eslint-disable-next-line no-restricted-imports +import { Provider } from 'react-redux'; +import { MemoryRouter } from 'react-router-dom'; +import { fireEvent, render, screen } from '@testing-library/react'; import JobDetails from 'container/InfraMonitoringK8s/Jobs/JobDetails/JobDetails'; -import { fireEvent, render, screen } from 'tests/test-utils'; +import { withNuqsTestingAdapter } from 'nuqs/adapters/testing'; +import store from 'store'; + +const queryClient = new QueryClient(); + +const Wrapper = withNuqsTestingAdapter({ searchParams: {} }); describe('JobDetails', () => { const mockJob = { @@ -16,7 +26,15 @@ describe('JobDetails', () => { it('should render modal with relevant metadata', () => { render( - , + + + + + + + + + , ); const jobNameElements = screen.getAllByText('test-job'); @@ -30,7 +48,15 @@ describe('JobDetails', () => { it('should render modal with 4 tabs', () => { render( - , + + + + + + + + + , ); const metricsTab = screen.getByText('Metrics'); @@ -48,7 +74,15 @@ describe('JobDetails', () => { it('default tab should be metrics', () => { render( - , + + + + + + + + + , ); const metricsTab = screen.getByRole('radio', { name: 'Metrics' }); @@ -57,7 +91,15 @@ describe('JobDetails', () => { it('should switch to events tab when events tab is clicked', () => { render( - , + + + + + + + + + , ); const eventsTab = screen.getByRole('radio', { name: 'Events' }); @@ -68,7 +110,15 @@ describe('JobDetails', () => { it('should close modal when close button is clicked', () => { render( - , + + + + + + + + + , ); const closeButton = screen.getByRole('button', { name: 'Close' }); diff --git a/frontend/src/container/InfraMonitoringK8s/__tests__/K8sHeader.test.tsx b/frontend/src/container/InfraMonitoringK8s/__tests__/K8sHeader.test.tsx new file mode 100644 index 00000000000..2f82774c0d5 --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/__tests__/K8sHeader.test.tsx @@ -0,0 +1,131 @@ +import setupCommonMocks from './commonMocks'; + +setupCommonMocks(); + +import { QueryClient, QueryClientProvider } from 'react-query'; +// eslint-disable-next-line no-restricted-imports +import { MemoryRouter } from 'react-router-dom'; +import { render, screen } from '@testing-library/react'; +import K8sHeader from 'container/InfraMonitoringK8s/K8sHeader'; +import { withNuqsTestingAdapter } from 'nuqs/adapters/testing'; + +import { INFRA_MONITORING_K8S_PARAMS_KEYS, K8sCategory } from '../constants'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, +}); + +describe('K8sHeader URL Parameter Parsing', () => { + const defaultProps = { + selectedGroupBy: [], + groupByOptions: [], + isLoadingGroupByFilters: false, + handleFiltersChange: jest.fn(), + handleGroupByChange: jest.fn(), + defaultAddedColumns: [], + handleFilterVisibilityChange: jest.fn(), + isFiltersVisible: true, + entity: K8sCategory.PODS, + showAutoRefresh: true, + }; + + const renderComponent = ( + searchParams?: string | Record, + ): ReturnType => { + const Wrapper = withNuqsTestingAdapter({ searchParams: searchParams ?? {} }); + return render( + + + + + + + , + ); + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render without crashing when no URL params', () => { + expect(() => renderComponent()).not.toThrow(); + expect(screen.getByText('Group by')).toBeInTheDocument(); + }); + + it('should render without crashing with valid filters in URL', () => { + const filters = { + items: [ + { + id: '1', + key: { key: 'k8s_namespace_name' }, + op: '=', + value: 'kube-system', + }, + ], + op: 'AND', + }; + + expect(() => + renderComponent({ + [INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS]: JSON.stringify(filters), + }), + ).not.toThrow(); + }); + + it('should render without crashing with malformed filters JSON', () => { + const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + expect(() => + renderComponent({ + [INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS]: 'invalid-json', + }), + ).not.toThrow(); + + consoleSpy.mockRestore(); + }); + + it('should handle filters with K8s container image values', () => { + const filters = { + items: [ + { + id: '1', + key: { key: 'k8s_container_image' }, + op: '=', + value: 'registry.k8s.io/coredns/coredns:v1.10.1', + }, + ], + op: 'AND', + }; + + expect(() => + renderComponent({ + [INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS]: JSON.stringify(filters), + }), + ).not.toThrow(); + }); + + it('should handle filters with percent signs in values', () => { + const filters = { + items: [ + { + id: '1', + key: { key: 'k8s_label' }, + op: '=', + value: 'cpu-usage-50%', + }, + ], + op: 'AND', + }; + + expect(() => + renderComponent({ + [INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS]: JSON.stringify(filters), + }), + ).not.toThrow(); + }); +}); diff --git a/frontend/src/container/InfraMonitoringK8s/__tests__/Namespaces/NamespaceDetails/NamespaceDetails.test.tsx b/frontend/src/container/InfraMonitoringK8s/__tests__/Namespaces/NamespaceDetails/NamespaceDetails.test.tsx index 560c9d31ef3..b6c8a71ee69 100644 --- a/frontend/src/container/InfraMonitoringK8s/__tests__/Namespaces/NamespaceDetails/NamespaceDetails.test.tsx +++ b/frontend/src/container/InfraMonitoringK8s/__tests__/Namespaces/NamespaceDetails/NamespaceDetails.test.tsx @@ -8,10 +8,13 @@ import { Provider } from 'react-redux'; import { MemoryRouter } from 'react-router-dom'; import { fireEvent, render, screen } from '@testing-library/react'; import NamespaceDetails from 'container/InfraMonitoringK8s/Namespaces/NamespaceDetails/NamespaceDetails'; +import { withNuqsTestingAdapter } from 'nuqs/adapters/testing'; import store from 'store'; const queryClient = new QueryClient(); +const Wrapper = withNuqsTestingAdapter({ searchParams: {} }); + describe('NamespaceDetails', () => { const mockNamespace = { namespaceName: 'test-namespace', @@ -23,17 +26,19 @@ describe('NamespaceDetails', () => { it('should render modal with relevant metadata', () => { render( - - - - - - - , + + + + + + + + + , ); const namespaceNameElements = screen.getAllByText('test-namespace'); @@ -47,17 +52,19 @@ describe('NamespaceDetails', () => { it('should render modal with 4 tabs', () => { render( - - - - - - - , + + + + + + + + + , ); const metricsTab = screen.getByText('Metrics'); @@ -75,17 +82,19 @@ describe('NamespaceDetails', () => { it('default tab should be metrics', () => { render( - - - - - - - , + + + + + + + + + , ); const metricsTab = screen.getByRole('radio', { name: 'Metrics' }); @@ -94,17 +103,19 @@ describe('NamespaceDetails', () => { it('should switch to events tab when events tab is clicked', () => { render( - - - - - - - , + + + + + + + + + , ); const eventsTab = screen.getByRole('radio', { name: 'Events' }); @@ -115,17 +126,19 @@ describe('NamespaceDetails', () => { it('should close modal when close button is clicked', () => { render( - - - - - - - , + + + + + + + + + , ); const closeButton = screen.getByRole('button', { name: 'Close' }); diff --git a/frontend/src/container/InfraMonitoringK8s/__tests__/Nodes/NodeDetails/NodeDetails.test.tsx b/frontend/src/container/InfraMonitoringK8s/__tests__/Nodes/NodeDetails/NodeDetails.test.tsx index 389de73a501..f50444e9340 100644 --- a/frontend/src/container/InfraMonitoringK8s/__tests__/Nodes/NodeDetails/NodeDetails.test.tsx +++ b/frontend/src/container/InfraMonitoringK8s/__tests__/Nodes/NodeDetails/NodeDetails.test.tsx @@ -8,10 +8,13 @@ import { Provider } from 'react-redux'; import { MemoryRouter } from 'react-router-dom'; import { fireEvent, render, screen } from '@testing-library/react'; import NodeDetails from 'container/InfraMonitoringK8s/Nodes/NodeDetails/NodeDetails'; +import { withNuqsTestingAdapter } from 'nuqs/adapters/testing'; import store from 'store'; const queryClient = new QueryClient(); +const Wrapper = withNuqsTestingAdapter({ searchParams: {} }); + describe('NodeDetails', () => { const mockNode = { meta: { @@ -23,13 +26,19 @@ describe('NodeDetails', () => { it('should render modal with relevant metadata', () => { render( - - - - - - - , + + + + + + + + + , ); const nodeNameElements = screen.getAllByText('test-node'); @@ -43,13 +52,19 @@ describe('NodeDetails', () => { it('should render modal with 4 tabs', () => { render( - - - - - - - , + + + + + + + + + , ); const metricsTab = screen.getByText('Metrics'); @@ -67,13 +82,19 @@ describe('NodeDetails', () => { it('default tab should be metrics', () => { render( - - - - - - - , + + + + + + + + + , ); const metricsTab = screen.getByRole('radio', { name: 'Metrics' }); @@ -82,13 +103,19 @@ describe('NodeDetails', () => { it('should switch to events tab when events tab is clicked', () => { render( - - - - - - - , + + + + + + + + + , ); const eventsTab = screen.getByRole('radio', { name: 'Events' }); @@ -99,13 +126,19 @@ describe('NodeDetails', () => { it('should close modal when close button is clicked', () => { render( - - - - - - - , + + + + + + + + + , ); const closeButton = screen.getByRole('button', { name: 'Close' }); diff --git a/frontend/src/container/InfraMonitoringK8s/__tests__/Pods/K8sPodsList.test.tsx b/frontend/src/container/InfraMonitoringK8s/__tests__/Pods/K8sPodsList.test.tsx new file mode 100644 index 00000000000..328d1fa5c77 --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/__tests__/Pods/K8sPodsList.test.tsx @@ -0,0 +1,155 @@ +/** + * Tests for URL parameter parsing in K8s Infra Monitoring components. + * + * These tests verify the fix for the double URL decoding bug where + * components were calling decodeURIComponent() on values already + * decoded by URLSearchParams.get(), causing crashes on K8s parameters + * with special characters. + */ + +import { getFiltersFromParams } from '../../commonUtils'; + +describe('K8sPodsList URL Parameter Parsing', () => { + describe('getFiltersFromParams', () => { + it('should return null when no filters in params', () => { + const searchParams = new URLSearchParams(); + const result = getFiltersFromParams(searchParams, 'filters'); + expect(result).toBeNull(); + }); + + it('should parse filters from URL params', () => { + const filters = { + items: [ + { + id: '1', + key: { key: 'k8s_namespace_name' }, + op: '=', + value: 'default', + }, + ], + op: 'AND', + }; + const searchParams = new URLSearchParams(); + searchParams.set('filters', JSON.stringify(filters)); + + const result = getFiltersFromParams(searchParams, 'filters'); + expect(result).toEqual(filters); + }); + + it('should handle URL-encoded filters (auto-decoded by URLSearchParams)', () => { + const filters = { + items: [ + { + id: '1', + key: { key: 'k8s_pod_name' }, + op: 'contains', + value: 'api-server', + }, + ], + op: 'AND', + }; + const encodedValue = encodeURIComponent(JSON.stringify(filters)); + const searchParams = new URLSearchParams(`filters=${encodedValue}`); + + const result = getFiltersFromParams(searchParams, 'filters'); + expect(result).toEqual(filters); + }); + + it('should return null on malformed JSON instead of crashing', () => { + const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + const searchParams = new URLSearchParams(); + searchParams.set('filters', '{invalid-json}'); + + const result = getFiltersFromParams(searchParams, 'filters'); + expect(result).toBeNull(); + consoleSpy.mockRestore(); + }); + + it('should handle filters with K8s container image names', () => { + const filters = { + items: [ + { + id: '1', + key: { key: 'k8s_container_name' }, + op: '=', + value: 'registry.k8s.io/coredns/coredns:v1.10.1', + }, + ], + op: 'AND', + }; + const encodedValue = encodeURIComponent(JSON.stringify(filters)); + const searchParams = new URLSearchParams(`filters=${encodedValue}`); + + const result = getFiltersFromParams(searchParams, 'filters'); + expect(result).toEqual(filters); + }); + }); + + describe('regression: double decoding issue', () => { + it('should not crash when URL params are already decoded by URLSearchParams', () => { + // The key bug: URLSearchParams.get() auto-decodes, so encoding once in URL + // means .get() returns decoded value. Old code called decodeURIComponent() + // again which could crash on certain characters. + + const filters = { + items: [ + { + id: '1', + key: { key: 'k8s_namespace_name' }, + op: '=', + value: 'kube-system', + }, + ], + op: 'AND', + }; + + const encodedValue = encodeURIComponent(JSON.stringify(filters)); + const searchParams = new URLSearchParams(`filters=${encodedValue}`); + + // This should work without crashing + const result = getFiltersFromParams(searchParams, 'filters'); + expect(result).toEqual(filters); + }); + + it('should handle values with percent signs in labels', () => { + // K8s labels might contain literal "%" characters like "cpu-usage-50%" + const filters = { + items: [ + { + id: '1', + key: { key: 'k8s_label' }, + op: '=', + value: 'cpu-50%', + }, + ], + op: 'AND', + }; + + const encodedValue = encodeURIComponent(JSON.stringify(filters)); + const searchParams = new URLSearchParams(`filters=${encodedValue}`); + + const result = getFiltersFromParams(searchParams, 'filters'); + expect(result).toEqual(filters); + }); + + it('should handle complex K8s deployment names with special chars', () => { + const filters = { + items: [ + { + id: '1', + key: { key: 'k8s_deployment_name' }, + op: '=', + value: 'nginx-ingress-controller', + }, + ], + op: 'AND', + }; + + const encodedValue = encodeURIComponent(JSON.stringify(filters)); + const searchParams = new URLSearchParams(`filters=${encodedValue}`); + + const result = getFiltersFromParams(searchParams, 'filters'); + expect(result).toEqual(filters); + }); + }); +}); diff --git a/frontend/src/container/InfraMonitoringK8s/__tests__/Pods/PodDetails/PodDetails.test.tsx b/frontend/src/container/InfraMonitoringK8s/__tests__/Pods/PodDetails/PodDetails.test.tsx index 81a51b33dd6..90f85360870 100644 --- a/frontend/src/container/InfraMonitoringK8s/__tests__/Pods/PodDetails/PodDetails.test.tsx +++ b/frontend/src/container/InfraMonitoringK8s/__tests__/Pods/PodDetails/PodDetails.test.tsx @@ -8,10 +8,13 @@ import { Provider } from 'react-redux'; import { MemoryRouter } from 'react-router-dom'; import { fireEvent, render, screen } from '@testing-library/react'; import PodDetails from 'container/InfraMonitoringK8s/Pods/PodDetails/PodDetails'; +import { withNuqsTestingAdapter } from 'nuqs/adapters/testing'; import store from 'store'; const queryClient = new QueryClient(); +const Wrapper = withNuqsTestingAdapter({ searchParams: {} }); + describe('PodDetails', () => { const mockPod = { podName: 'test-pod', @@ -25,13 +28,15 @@ describe('PodDetails', () => { it('should render modal with relevant metadata', () => { render( - - - - - - - , + + + + + + + + + , ); const clusterNameElements = screen.getAllByText('test-cluster'); @@ -49,13 +54,15 @@ describe('PodDetails', () => { it('should render modal with 4 tabs', () => { render( - - - - - - - , + + + + + + + + + , ); const metricsTab = screen.getByText('Metrics'); @@ -73,13 +80,15 @@ describe('PodDetails', () => { it('default tab should be metrics', () => { render( - - - - - - - , + + + + + + + + + , ); const metricsTab = screen.getByRole('radio', { name: 'Metrics' }); @@ -88,13 +97,15 @@ describe('PodDetails', () => { it('should switch to events tab when events tab is clicked', () => { render( - - - - - - - , + + + + + + + + + , ); const eventsTab = screen.getByRole('radio', { name: 'Events' }); @@ -105,13 +116,15 @@ describe('PodDetails', () => { it('should close modal when close button is clicked', () => { render( - - - - - - - , + + + + + + + + + , ); const closeButton = screen.getByRole('button', { name: 'Close' }); diff --git a/frontend/src/container/InfraMonitoringK8s/__tests__/StatefulSets/StatefulSetDetails/StatefulSetDetails.test.tsx b/frontend/src/container/InfraMonitoringK8s/__tests__/StatefulSets/StatefulSetDetails/StatefulSetDetails.test.tsx index ae280203a51..85139c7d226 100644 --- a/frontend/src/container/InfraMonitoringK8s/__tests__/StatefulSets/StatefulSetDetails/StatefulSetDetails.test.tsx +++ b/frontend/src/container/InfraMonitoringK8s/__tests__/StatefulSets/StatefulSetDetails/StatefulSetDetails.test.tsx @@ -4,14 +4,16 @@ setupCommonMocks(); import { QueryClient, QueryClientProvider } from 'react-query'; // eslint-disable-next-line no-restricted-imports -import { Provider } from 'react-redux'; import { MemoryRouter } from 'react-router-dom'; -import { fireEvent, render, screen } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import StatefulSetDetails from 'container/InfraMonitoringK8s/StatefulSets/StatefulSetDetails/StatefulSetDetails'; -import store from 'store'; +import { withNuqsTestingAdapter } from 'nuqs/adapters/testing'; +import { userEvent } from 'tests/test-utils'; const queryClient = new QueryClient(); +const Wrapper = withNuqsTestingAdapter({ searchParams: {} }); + describe('StatefulSetDetails', () => { const mockStatefulSet = { meta: { @@ -23,8 +25,8 @@ describe('StatefulSetDetails', () => { it('should render modal with relevant metadata', () => { render( - - + + { onClose={mockOnClose} /> - - , + + , ); const statefulSetNameElements = screen.getAllByText('test-stateful-set'); @@ -47,8 +49,8 @@ describe('StatefulSetDetails', () => { it('should render modal with 4 tabs', () => { render( - - + + { onClose={mockOnClose} /> - - , + + , ); const metricsTab = screen.getByText('Metrics'); @@ -75,8 +77,8 @@ describe('StatefulSetDetails', () => { it('default tab should be metrics', () => { render( - - + + { onClose={mockOnClose} /> - - , + + , ); const metricsTab = screen.getByRole('radio', { name: 'Metrics' }); expect(metricsTab).toBeChecked(); }); - it('should switch to events tab when events tab is clicked', () => { + it('should switch to events tab when events tab is clicked', async () => { render( - - + + { onClose={mockOnClose} /> - - , + + , ); const eventsTab = screen.getByRole('radio', { name: 'Events' }); expect(eventsTab).not.toBeChecked(); - fireEvent.click(eventsTab); + await userEvent.click(eventsTab, { pointerEventsCheck: 0 }); expect(eventsTab).toBeChecked(); }); - it('should close modal when close button is clicked', () => { + it('should close modal when close button is clicked', async () => { render( - - + + { onClose={mockOnClose} /> - - , + + , ); const closeButton = screen.getByRole('button', { name: 'Close' }); - fireEvent.click(closeButton); + await userEvent.click(closeButton); expect(mockOnClose).toHaveBeenCalled(); }); }); diff --git a/frontend/src/container/InfraMonitoringK8s/__tests__/Volumes/K8sVolumesList.test.tsx b/frontend/src/container/InfraMonitoringK8s/__tests__/Volumes/K8sVolumesList.test.tsx index f68ba507fc4..d2d2e1f1a64 100644 --- a/frontend/src/container/InfraMonitoringK8s/__tests__/Volumes/K8sVolumesList.test.tsx +++ b/frontend/src/container/InfraMonitoringK8s/__tests__/Volumes/K8sVolumesList.test.tsx @@ -4,14 +4,13 @@ setupCommonMocks(); import { QueryClient, QueryClientProvider } from 'react-query'; // eslint-disable-next-line no-restricted-imports -import { Provider } from 'react-redux'; import { MemoryRouter } from 'react-router-dom'; import { render, waitFor } from '@testing-library/react'; import { FeatureKeys } from 'constants/features'; import K8sVolumesList from 'container/InfraMonitoringK8s/Volumes/K8sVolumesList'; import { rest, server } from 'mocks-server/server'; +import { NuqsTestingAdapter } from 'nuqs/adapters/testing'; import { IAppContext, IUser } from 'providers/App/types'; -import store from 'store'; import { LicenseResModel } from 'types/api/licensesV3/getActive'; const queryClient = new QueryClient({ @@ -81,8 +80,8 @@ describe('K8sVolumesList - useGetAggregateKeys Category Regression', () => { it('should call aggregate keys API with k8s_volume_capacity', async () => { render( - - + + { quickFiltersLastUpdated={-1} /> - - , + + , ); await waitFor(() => { @@ -130,8 +129,8 @@ describe('K8sVolumesList - useGetAggregateKeys Category Regression', () => { } as IAppContext); render( - - + + { quickFiltersLastUpdated={-1} /> - - , + + , ); await waitFor(() => { diff --git a/frontend/src/container/InfraMonitoringK8s/__tests__/commonMocks.ts b/frontend/src/container/InfraMonitoringK8s/__tests__/commonMocks.ts index 39744c446ed..8216e5ca685 100644 --- a/frontend/src/container/InfraMonitoringK8s/__tests__/commonMocks.ts +++ b/frontend/src/container/InfraMonitoringK8s/__tests__/commonMocks.ts @@ -1,3 +1,4 @@ +import { createElement } from 'react'; import * as appContextHooks from 'providers/App/App'; import * as timezoneHooks from 'providers/Timezone'; import { LicenseEvent } from 'types/api/licensesV3/getActive'; @@ -45,14 +46,6 @@ const setupCommonMocks = (): void => { jest.mock('react-router-dom-v5-compat', () => ({ ...jest.requireActual('react-router-dom-v5-compat'), - useSearchParams: jest.fn().mockReturnValue([ - { - get: jest.fn(), - entries: jest.fn(() => []), - set: jest.fn(), - }, - jest.fn(), - ]), useNavigationType: (): any => 'PUSH', })); @@ -103,6 +96,15 @@ const setupCommonMocks = (): void => { safeNavigate: jest.fn(), }), })); + + // TODO: Remove this when https://github.com/SigNoz/engineering-pod/issues/4253 + jest.mock('container/TopNav/DateTimeSelectionV2', () => { + return { + __esModule: true, + default: (): React.ReactElement => + createElement('div', { 'data-testid': 'datetime-selection' }), + }; + }); }; export default setupCommonMocks; diff --git a/frontend/src/container/InfraMonitoringK8s/commonUtils.tsx b/frontend/src/container/InfraMonitoringK8s/commonUtils.tsx index 7fb8c5e69fc..1cda94fa05e 100644 --- a/frontend/src/container/InfraMonitoringK8s/commonUtils.tsx +++ b/frontend/src/container/InfraMonitoringK8s/commonUtils.tsx @@ -12,11 +12,7 @@ import { TagFilterItem, } from 'types/api/queryBuilder/queryBuilderData'; -import { - getInvalidValueTooltipText, - INFRA_MONITORING_K8S_PARAMS_KEYS, - K8sCategory, -} from './constants'; +import { getInvalidValueTooltipText, K8sCategory } from './constants'; /** * Converts size in bytes to a human-readable string with appropriate units @@ -254,27 +250,6 @@ export const filterDuplicateFilters = ( return uniqueFilters; }; -export const getOrderByFromParams = ( - searchParams: URLSearchParams, - returnNullAsDefault = false, -): { - columnName: string; - order: 'asc' | 'desc'; -} | null => { - const orderByFromParams = searchParams.get( - INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY, - ); - if (orderByFromParams) { - const decoded = decodeURIComponent(orderByFromParams); - const parsed = JSON.parse(decoded); - return parsed as { columnName: string; order: 'asc' | 'desc' }; - } - if (returnNullAsDefault) { - return null; - } - return { columnName: 'cpu', order: 'desc' }; -}; - export const getFiltersFromParams = ( searchParams: URLSearchParams, queryKey: string, @@ -282,10 +257,9 @@ export const getFiltersFromParams = ( const filtersFromParams = searchParams.get(queryKey); if (filtersFromParams) { try { - const decoded = decodeURIComponent(filtersFromParams); - const parsed = JSON.parse(decoded); + const parsed = JSON.parse(filtersFromParams); return parsed as IBuilderQuery['filters']; - } catch (error) { + } catch { return null; } } diff --git a/frontend/src/container/InfraMonitoringK8s/hooks.ts b/frontend/src/container/InfraMonitoringK8s/hooks.ts new file mode 100644 index 00000000000..bf6f689eb4f --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/hooks.ts @@ -0,0 +1,224 @@ +import { VIEWS } from 'components/HostMetricsDetail/constants'; +import { + Options, + parseAsInteger, + parseAsJson, + parseAsString, + useQueryState, + UseQueryStateReturn, +} from 'nuqs'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { + IBuilderQuery, + TagFilter, +} from 'types/api/queryBuilder/queryBuilderData'; +import { parseAsJsonNoValidate } from 'utils/nuqsParsers'; + +import { INFRA_MONITORING_K8S_PARAMS_KEYS, K8sCategories } from './constants'; +import { orderBySchema, OrderBySchemaType } from './schemas'; + +const defaultFilters: IBuilderQuery['filters'] = { items: [], op: 'and' }; +const defaultNuqsOptions: Options = { + history: 'push', +}; + +export const useInfraMonitoringCurrentPage = (): UseQueryStateReturn< + number, + number +> => + useQueryState( + INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE, + parseAsInteger.withDefault(1).withOptions(defaultNuqsOptions), + ); + +export const useInfraMonitoringOrderBy = (): UseQueryStateReturn< + OrderBySchemaType, + OrderBySchemaType +> => + useQueryState( + INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY, + parseAsJson(orderBySchema).withOptions(defaultNuqsOptions), + ); + +export const useInfraMonitoringOrderByHosts = (): UseQueryStateReturn< + OrderBySchemaType, + OrderBySchemaType +> => + useQueryState( + INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY, + parseAsJson(orderBySchema) + .withDefault({ + columnName: 'cpu', + order: 'desc', + }) + .withOptions(defaultNuqsOptions), + ); + +export const useInfraMonitoringGroupBy = (): UseQueryStateReturn< + BaseAutocompleteData[], + [] +> => + useQueryState( + INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY, + parseAsJsonNoValidate() + .withDefault([]) + .withOptions(defaultNuqsOptions), + ); + +export const useInfraMonitoringView = (): UseQueryStateReturn => + useQueryState( + INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW, + parseAsString.withDefault(VIEWS.METRICS).withOptions(defaultNuqsOptions), + ); + +export const useInfraMonitoringLogFilters = (): UseQueryStateReturn< + TagFilter, + undefined +> => + useQueryState( + INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS, + parseAsJsonNoValidate().withOptions( + defaultNuqsOptions, + ), + ); + +export const useInfraMonitoringTracesFilters = (): UseQueryStateReturn< + TagFilter, + undefined +> => + useQueryState( + INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS, + parseAsJsonNoValidate().withOptions( + defaultNuqsOptions, + ), + ); + +export const useInfraMonitoringEventsFilters = (): UseQueryStateReturn< + TagFilter, + undefined +> => + useQueryState( + INFRA_MONITORING_K8S_PARAMS_KEYS.EVENTS_FILTERS, + parseAsJsonNoValidate().withOptions( + defaultNuqsOptions, + ), + ); + +export const useInfraMonitoringCategory = (): UseQueryStateReturn< + string, + string +> => + useQueryState( + INFRA_MONITORING_K8S_PARAMS_KEYS.CATEGORY, + parseAsString.withDefault(K8sCategories.PODS).withOptions(defaultNuqsOptions), + ); + +export const useInfraMonitoringFilters = (): UseQueryStateReturn< + string, + string +> => + useQueryState( + INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS, + parseAsString.withDefault('').withOptions(defaultNuqsOptions), + ); + +export const useInfraMonitoringFiltersK8s = (): UseQueryStateReturn< + TagFilter, + undefined +> => + useQueryState( + INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS, + parseAsJsonNoValidate().withOptions(defaultNuqsOptions), + ); + +export const useInfraMonitoringFiltersHosts = (): UseQueryStateReturn< + TagFilter, + TagFilter | undefined +> => + useQueryState( + INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS, + parseAsJsonNoValidate() + .withDefault(defaultFilters) + .withOptions(defaultNuqsOptions), + ); + +export const useInfraMonitoringClusterName = (): UseQueryStateReturn< + string, + string +> => + useQueryState( + INFRA_MONITORING_K8S_PARAMS_KEYS.CLUSTER_NAME, + parseAsString.withDefault('').withOptions(defaultNuqsOptions), + ); + +export const useInfraMonitoringDaemonSetUID = (): UseQueryStateReturn< + string, + undefined +> => + useQueryState( + INFRA_MONITORING_K8S_PARAMS_KEYS.DAEMONSET_UID, + parseAsString.withOptions(defaultNuqsOptions), + ); + +export const useInfraMonitoringDeploymentUID = (): UseQueryStateReturn< + string, + undefined +> => + useQueryState( + INFRA_MONITORING_K8S_PARAMS_KEYS.DEPLOYMENT_UID, + parseAsString.withOptions(defaultNuqsOptions), + ); + +export const useInfraMonitoringJobUID = (): UseQueryStateReturn< + string, + undefined +> => + useQueryState( + INFRA_MONITORING_K8S_PARAMS_KEYS.JOB_UID, + parseAsString.withOptions(defaultNuqsOptions), + ); + +export const useInfraMonitoringNamespaceUID = (): UseQueryStateReturn< + string, + undefined +> => + useQueryState( + INFRA_MONITORING_K8S_PARAMS_KEYS.NAMESPACE_UID, + parseAsString.withOptions(defaultNuqsOptions), + ); + +export const useInfraMonitoringNodeUID = (): UseQueryStateReturn< + string, + undefined +> => + useQueryState( + INFRA_MONITORING_K8S_PARAMS_KEYS.NODE_UID, + parseAsString.withOptions(defaultNuqsOptions), + ); + +export const useInfraMonitoringPodUID = (): UseQueryStateReturn< + string, + undefined +> => + useQueryState( + INFRA_MONITORING_K8S_PARAMS_KEYS.POD_UID, + parseAsString.withOptions(defaultNuqsOptions), + ); + +export const useInfraMonitoringStatefulSetUID = (): UseQueryStateReturn< + string, + undefined +> => + useQueryState( + INFRA_MONITORING_K8S_PARAMS_KEYS.STATEFULSET_UID, + parseAsString.withOptions(defaultNuqsOptions), + ); + +export const useInfraMonitoringVolumeUID = (): UseQueryStateReturn< + string, + undefined +> => + useQueryState( + INFRA_MONITORING_K8S_PARAMS_KEYS.VOLUME_UID, + parseAsString.withOptions(defaultNuqsOptions), + ); diff --git a/frontend/src/container/InfraMonitoringK8s/schemas.ts b/frontend/src/container/InfraMonitoringK8s/schemas.ts new file mode 100644 index 00000000000..2b0df2541ff --- /dev/null +++ b/frontend/src/container/InfraMonitoringK8s/schemas.ts @@ -0,0 +1,10 @@ +import { z } from 'zod'; + +export const orderBySchema = z + .object({ + columnName: z.string(), + order: z.enum(['asc', 'desc']), + }) + .nullable(); + +export type OrderBySchemaType = z.infer; diff --git a/frontend/src/container/InfraMonitoringK8s/utils.tsx b/frontend/src/container/InfraMonitoringK8s/utils.tsx index 2a65d29c24a..d97a1455235 100644 --- a/frontend/src/container/InfraMonitoringK8s/utils.tsx +++ b/frontend/src/container/InfraMonitoringK8s/utils.tsx @@ -1,7 +1,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { Color } from '@signozhq/design-tokens'; -import { Tag, Tooltip } from 'antd'; -import { TableColumnType as ColumnType } from 'antd'; +import { TableColumnType as ColumnType, Tag, Tooltip } from 'antd'; import get from 'api/browser/localstorage/get'; import set from 'api/browser/localstorage/set'; import { @@ -17,6 +16,7 @@ import { ValidateColumnValueWrapper, } from './commonUtils'; import { DEFAULT_PAGE_SIZE, K8sCategory } from './constants'; +import { OrderBySchemaType } from './schemas'; import './InfraMonitoringK8s.styles.scss'; @@ -148,7 +148,7 @@ export const dummyColumnConfig = { className: 'column column-dummy', }; -const columnsConfig = [ +const columnsConfig: ColumnType[] = [ { title:
Pod Name
, dataIndex: 'podName', @@ -231,7 +231,7 @@ const columnsConfig = [ // }, ]; -export const namespaceColumnConfig = { +export const namespaceColumnConfig: ColumnType = { title:
Namespace
, dataIndex: 'namespace', key: 'namespace', @@ -242,7 +242,7 @@ export const namespaceColumnConfig = { className: 'column column-namespace', }; -export const nodeColumnConfig = { +export const nodeColumnConfig: ColumnType = { title:
Node
, dataIndex: 'node', key: 'node', @@ -253,7 +253,7 @@ export const nodeColumnConfig = { className: 'column column-node', }; -export const clusterColumnConfig = { +export const clusterColumnConfig: ColumnType = { title:
Cluster
, dataIndex: 'cluster', key: 'cluster', @@ -264,7 +264,7 @@ export const clusterColumnConfig = { className: 'column column-cluster', }; -export const columnConfigMap = { +export const columnConfigMap: Record> = { namespace: namespaceColumnConfig, node: nodeColumnConfig, cluster: clusterColumnConfig, @@ -273,8 +273,9 @@ export const columnConfigMap = { export const getK8sPodsListColumns = ( addedColumns: IEntityColumn[], groupBy: IBuilderQuery['groupBy'], + defaultOrderBy: OrderBySchemaType, ): ColumnType[] => { - const updatedColumnsConfig = [...columnsConfig]; + const updatedColumnsConfig: ColumnType[] = [...columnsConfig]; for (const column of addedColumns) { const config = columnConfigMap[column.id as keyof typeof columnConfigMap]; @@ -293,7 +294,14 @@ export const getK8sPodsListColumns = ( return filteredColumns as ColumnType[]; } - return updatedColumnsConfig as ColumnType[]; + for (const column of updatedColumnsConfig) { + if (column.sorter && column.key === defaultOrderBy?.columnName) { + column.defaultSortOrder = + defaultOrderBy?.order === 'asc' ? 'ascend' : 'descend'; + } + } + + return updatedColumnsConfig; }; const dotToUnder: Record = { diff --git a/frontend/src/utils/nuqsParsers.ts b/frontend/src/utils/nuqsParsers.ts new file mode 100644 index 00000000000..5caab92c5a0 --- /dev/null +++ b/frontend/src/utils/nuqsParsers.ts @@ -0,0 +1,16 @@ +import { createParser, SingleParserBuilder } from 'nuqs'; + +export function parseAsJsonNoValidate(): SingleParserBuilder { + return createParser({ + parse: (query: string): T | null => { + try { + return JSON.parse(query) as T; + } catch { + return null; + } + }, + serialize: (value: T): string => JSON.stringify(value), + eq: (a: T, b: T): boolean => + a === b || JSON.stringify(a) === JSON.stringify(b), + }); +} diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 6937c75d6be..b004e5073f4 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -20381,6 +20381,11 @@ yocto-queue@^1.0.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.2.2.tgz#3e09c95d3f1aa89a58c114c99223edf639152c00" integrity sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ== +zod@4.3.6: + version "4.3.6" + resolved "https://registry.yarnpkg.com/zod/-/zod-4.3.6.tgz#89c56e0aa7d2b05107d894412227087885ab112a" + integrity sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg== + zustand@5.0.11: version "5.0.11" resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.11.tgz#99f912e590de1ca9ce6c6d1cab6cdb1f034ab494" From 37d202de92071754303df21a1f98cd1b36c14b5c Mon Sep 17 00:00:00 2001 From: Pandey Date: Mon, 30 Mar 2026 20:16:50 +0530 Subject: [PATCH 50/78] feat(audit): add auditortypes package (#10761) * feat(audit): add auditortypes package with event struct, store interface, and config Foundational type package for the audit log system with zero dependencies on other audit packages. Includes AuditEvent struct, Store interface (single Emit method), Config with defaults, constants for action/outcome/principal/category, and EventName derivation helper. * fix(audit): address PR review feedback on auditortypes - Remove non-CUD actions (login/logout/lock/unlock/revoke), keep only create/update/delete - Replace pastTenseMap with pastTense field on Action struct - Make EventName a typed struct with NewEventName() constructor - Rename enum vars to ActionCategory* prefix (ActionCategoryAccessControl, etc.) - Add IEC 62443 reference link to ActionCategory comment - Add TODO on PrincipalType to use coretypes once available - Rename types.go to event.go, merge Store interface into it - Reorder AuditEvent fields to follow OTel LogRecord structure - Delete config.go (belongs with auditor service, not types) - Delete store.go (merged into event.go) * fix(audit): remove redundant test files per review feedback --- pkg/types/auditortypes/action.go | 28 +++++++++++++++ pkg/types/auditortypes/category.go | 23 +++++++++++++ pkg/types/auditortypes/event.go | 51 ++++++++++++++++++++++++++++ pkg/types/auditortypes/event_name.go | 24 +++++++++++++ pkg/types/auditortypes/outcome.go | 18 ++++++++++ pkg/types/auditortypes/principal.go | 23 +++++++++++++ 6 files changed, 167 insertions(+) create mode 100644 pkg/types/auditortypes/action.go create mode 100644 pkg/types/auditortypes/category.go create mode 100644 pkg/types/auditortypes/event.go create mode 100644 pkg/types/auditortypes/event_name.go create mode 100644 pkg/types/auditortypes/outcome.go create mode 100644 pkg/types/auditortypes/principal.go diff --git a/pkg/types/auditortypes/action.go b/pkg/types/auditortypes/action.go new file mode 100644 index 00000000000..370e869d03b --- /dev/null +++ b/pkg/types/auditortypes/action.go @@ -0,0 +1,28 @@ +package auditortypes + +import "github.com/SigNoz/signoz/pkg/valuer" + +// Action represents what was done. +type Action struct { + valuer.String + pastTense string +} + +var ( + ActionCreate = Action{valuer.NewString("create"), "created"} + ActionUpdate = Action{valuer.NewString("update"), "updated"} + ActionDelete = Action{valuer.NewString("delete"), "deleted"} +) + +func (Action) Enum() []any { + return []any{ + ActionCreate, + ActionUpdate, + ActionDelete, + } +} + +// PastTense returns the past-tense form of the action for use in EventName. +func (a Action) PastTense() string { + return a.pastTense +} diff --git a/pkg/types/auditortypes/category.go b/pkg/types/auditortypes/category.go new file mode 100644 index 00000000000..f583b747e52 --- /dev/null +++ b/pkg/types/auditortypes/category.go @@ -0,0 +1,23 @@ +package auditortypes + +import "github.com/SigNoz/signoz/pkg/valuer" + +// ActionCategory classifies the audit event per IEC 62443. +// See https://www.iec.ch/blog/understanding-iec-62443 for the standard reference. +type ActionCategory struct{ valuer.String } + +var ( + ActionCategoryAccessControl = ActionCategory{valuer.NewString("access_control")} + ActionCategoryConfigurationChange = ActionCategory{valuer.NewString("configuration_change")} + ActionCategoryDataAccess = ActionCategory{valuer.NewString("data_access")} + ActionCategorySystemEvent = ActionCategory{valuer.NewString("system_event")} +) + +func (ActionCategory) Enum() []any { + return []any{ + ActionCategoryAccessControl, + ActionCategoryConfigurationChange, + ActionCategoryDataAccess, + ActionCategorySystemEvent, + } +} diff --git a/pkg/types/auditortypes/event.go b/pkg/types/auditortypes/event.go new file mode 100644 index 00000000000..3e18d951279 --- /dev/null +++ b/pkg/types/auditortypes/event.go @@ -0,0 +1,51 @@ +package auditortypes + +import ( + "context" + "time" +) + +// AuditEvent represents a single audit log event. +// Fields are ordered following the OTel LogRecord structure. +type AuditEvent struct { + // OTel LogRecord intrinsic fields + Timestamp time.Time `json:"timestamp"` + TraceID string `json:"traceId,omitempty"` + SpanID string `json:"spanId,omitempty"` + Body string `json:"body"` + EventName EventName `json:"eventName"` + + // Audit attributes — Principal (Who) + PrincipalID string `json:"principalId"` + PrincipalEmail string `json:"principalEmail"` + PrincipalType PrincipalType `json:"principalType"` + PrincipalOrgID string `json:"principalOrgId"` + IdentNProvider string `json:"identnProvider,omitempty"` + + // Audit attributes — Action (What) + Action Action `json:"action"` + ActionCategory ActionCategory `json:"actionCategory"` + Outcome Outcome `json:"outcome"` + + // Audit attributes — Resource (On What) + ResourceName string `json:"resourceName"` + ResourceID string `json:"resourceId,omitempty"` + + // Audit attributes — Error (When outcome is failure) + ErrorType string `json:"errorType,omitempty"` + ErrorCode string `json:"errorCode,omitempty"` + ErrorMessage string `json:"errorMessage,omitempty"` + + // Transport Context (Where/How) + HTTPMethod string `json:"httpMethod,omitempty"` + HTTPRoute string `json:"httpRoute,omitempty"` + HTTPStatusCode int `json:"httpStatusCode,omitempty"` + URLPath string `json:"urlPath,omitempty"` + ClientAddress string `json:"clientAddress,omitempty"` + UserAgent string `json:"userAgent,omitempty"` +} + +// Store is the minimal interface for emitting audit events. +type Store interface { + Emit(ctx context.Context, event AuditEvent) error +} diff --git a/pkg/types/auditortypes/event_name.go b/pkg/types/auditortypes/event_name.go new file mode 100644 index 00000000000..9cf0ea743c6 --- /dev/null +++ b/pkg/types/auditortypes/event_name.go @@ -0,0 +1,24 @@ +package auditortypes + +// EventName is a typed wrapper for audit event names, ensuring not every +// string qualifies as an event name. +type EventName struct { + s string +} + +// NewEventName derives the audit event name from a resource name and action. +// Format: {resource_name}.{pastTense(action)} +// +// Examples: +// +// NewEventName("dashboard", ActionCreate) → "dashboard.created" +// NewEventName("dashboard", ActionUpdate) → "dashboard.updated" +// NewEventName("user.role", ActionUpdate) → "user.role.updated" +func NewEventName(resourceName string, action Action) EventName { + return EventName{s: resourceName + "." + action.PastTense()} +} + +// String returns the string representation of the event name. +func (e EventName) String() string { + return e.s +} diff --git a/pkg/types/auditortypes/outcome.go b/pkg/types/auditortypes/outcome.go new file mode 100644 index 00000000000..3eeeae46e0c --- /dev/null +++ b/pkg/types/auditortypes/outcome.go @@ -0,0 +1,18 @@ +package auditortypes + +import "github.com/SigNoz/signoz/pkg/valuer" + +// Outcome represents the result of an audited operation. +type Outcome struct{ valuer.String } + +var ( + OutcomeSuccess = Outcome{valuer.NewString("success")} + OutcomeFailure = Outcome{valuer.NewString("failure")} +) + +func (Outcome) Enum() []any { + return []any{ + OutcomeSuccess, + OutcomeFailure, + } +} diff --git a/pkg/types/auditortypes/principal.go b/pkg/types/auditortypes/principal.go new file mode 100644 index 00000000000..b4b7cd0d2b4 --- /dev/null +++ b/pkg/types/auditortypes/principal.go @@ -0,0 +1,23 @@ +package auditortypes + +import "github.com/SigNoz/signoz/pkg/valuer" + +// PrincipalType identifies the kind of actor that performed the action. +// TODO: use PrincipalType from coretypes once the coretypes package is available. +type PrincipalType struct{ valuer.String } + +var ( + PrincipalTypeUser = PrincipalType{valuer.NewString("user")} + PrincipalTypeServiceAccount = PrincipalType{valuer.NewString("service_account")} + PrincipalTypeSystem = PrincipalType{valuer.NewString("system")} + PrincipalTypeAnonymous = PrincipalType{valuer.NewString("anonymous")} +) + +func (PrincipalType) Enum() []any { + return []any{ + PrincipalTypeUser, + PrincipalTypeServiceAccount, + PrincipalTypeSystem, + PrincipalTypeAnonymous, + } +} From 1f43feaf3c16521d67fa831338495ea0375cc342 Mon Sep 17 00:00:00 2001 From: Phuc <119803257+pauln17@users.noreply.github.com> Date: Mon, 30 Mar 2026 11:49:01 -0400 Subject: [PATCH 51/78] refactor(time-range): replace hardcoded 30m defaults with const (#10748) * refactor: replace hardcoded 30m defaults with DEFAULT_TIME_RANGE * refactor: use DEFAULT_TIME_RANGE in home.tsx * revert: restore mock data before DEFAULT_TIME_RANGE changes --- .../src/api/dashboard/public/createPublicDashboard.ts | 3 ++- .../src/api/dashboard/public/updatePublicDashboard.ts | 3 ++- .../PublicDashboard/__tests__/PublicDashboard.test.tsx | 2 +- .../DashboardSettings/PublicDashboard/index.tsx | 7 ++++--- frontend/src/container/Home/Home.tsx | 9 +++++---- .../PublicDashboardContainer.tsx | 5 +++-- .../__tests__/PublicDashboardContainer.test.tsx | 2 +- .../container/TopNav/DateTimeSelectionV2/constants.ts | 4 +++- frontend/src/pages/AlertDetails/hooks.tsx | 3 +-- 9 files changed, 22 insertions(+), 16 deletions(-) diff --git a/frontend/src/api/dashboard/public/createPublicDashboard.ts b/frontend/src/api/dashboard/public/createPublicDashboard.ts index bb45d778857..66ef406c768 100644 --- a/frontend/src/api/dashboard/public/createPublicDashboard.ts +++ b/frontend/src/api/dashboard/public/createPublicDashboard.ts @@ -1,6 +1,7 @@ import axios from 'api'; import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2'; import { AxiosError } from 'axios'; +import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants'; import { ErrorV2Resp, SuccessResponseV2 } from 'types/api'; import { CreatePublicDashboardProps } from 'types/api/dashboard/public/create'; @@ -8,7 +9,7 @@ const createPublicDashboard = async ( props: CreatePublicDashboardProps, ): Promise> => { - const { dashboardId, timeRangeEnabled = false, defaultTimeRange = '30m' } = props; + const { dashboardId, timeRangeEnabled = false, defaultTimeRange = DEFAULT_TIME_RANGE } = props; try { const response = await axios.post( diff --git a/frontend/src/api/dashboard/public/updatePublicDashboard.ts b/frontend/src/api/dashboard/public/updatePublicDashboard.ts index 6daacf5118e..976cac55b0b 100644 --- a/frontend/src/api/dashboard/public/updatePublicDashboard.ts +++ b/frontend/src/api/dashboard/public/updatePublicDashboard.ts @@ -1,6 +1,7 @@ import axios from 'api'; import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2'; import { AxiosError } from 'axios'; +import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants'; import { ErrorV2Resp, SuccessResponseV2 } from 'types/api'; import { UpdatePublicDashboardProps } from 'types/api/dashboard/public/update'; @@ -8,7 +9,7 @@ const updatePublicDashboard = async ( props: UpdatePublicDashboardProps, ): Promise> => { - const { dashboardId, timeRangeEnabled = false, defaultTimeRange = '30m' } = props; + const { dashboardId, timeRangeEnabled = false, defaultTimeRange = DEFAULT_TIME_RANGE } = props; try { const response = await axios.put( diff --git a/frontend/src/container/DashboardContainer/DashboardSettings/PublicDashboard/__tests__/PublicDashboard.test.tsx b/frontend/src/container/DashboardContainer/DashboardSettings/PublicDashboard/__tests__/PublicDashboard.test.tsx index 1ebbfd889fe..6abdee67ebf 100644 --- a/frontend/src/container/DashboardContainer/DashboardSettings/PublicDashboard/__tests__/PublicDashboard.test.tsx +++ b/frontend/src/container/DashboardContainer/DashboardSettings/PublicDashboard/__tests__/PublicDashboard.test.tsx @@ -1,6 +1,7 @@ import { useCopyToClipboard } from 'react-use'; import { toast } from '@signozhq/sonner'; import { fireEvent, within } from '@testing-library/react'; +import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants'; import { StatusCodes } from 'http-status-codes'; import { publishedPublicDashboardMeta, @@ -33,7 +34,6 @@ const mockToast = jest.mocked(toast); // Test constants const MOCK_DASHBOARD_ID = 'test-dashboard-id'; const MOCK_PUBLIC_PATH = '/public/dashboard/test-dashboard-id'; -const DEFAULT_TIME_RANGE = '30m'; const DASHBOARD_VARIABLES_WARNING = "Dashboard variables won't work in public dashboards"; diff --git a/frontend/src/container/DashboardContainer/DashboardSettings/PublicDashboard/index.tsx b/frontend/src/container/DashboardContainer/DashboardSettings/PublicDashboard/index.tsx index 629a4fc44d5..6527b12474e 100644 --- a/frontend/src/container/DashboardContainer/DashboardSettings/PublicDashboard/index.tsx +++ b/frontend/src/container/DashboardContainer/DashboardSettings/PublicDashboard/index.tsx @@ -7,6 +7,7 @@ import { Button, Select, Typography } from 'antd'; import createPublicDashboardAPI from 'api/dashboard/public/createPublicDashboard'; import revokePublicDashboardAccessAPI from 'api/dashboard/public/revokePublicDashboardAccess'; import updatePublicDashboardAPI from 'api/dashboard/public/updatePublicDashboard'; +import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants'; import { useGetPublicDashboardMeta } from 'hooks/dashboard/useGetPublicDashboardMeta'; import { useGetTenantLicense } from 'hooks/useGetTenantLicense'; import { Copy, ExternalLink, Globe, Info, Loader2, Trash } from 'lucide-react'; @@ -56,7 +57,7 @@ function PublicDashboardSetting(): JSX.Element { PublicDashboardMetaProps | undefined >(undefined); const [timeRangeEnabled, setTimeRangeEnabled] = useState(true); - const [defaultTimeRange, setDefaultTimeRange] = useState('30m'); + const [defaultTimeRange, setDefaultTimeRange] = useState(DEFAULT_TIME_RANGE); const [, setCopyPublicDashboardURL] = useCopyToClipboard(); const { selectedDashboard } = useDashboardStore(); @@ -99,7 +100,7 @@ function PublicDashboardSetting(): JSX.Element { console.error('Error getting public dashboard', errorPublicDashboard); setPublicDashboardData(undefined); setTimeRangeEnabled(true); - setDefaultTimeRange('30m'); + setDefaultTimeRange(DEFAULT_TIME_RANGE); } }, [publicDashboardResponse, errorPublicDashboard]); @@ -109,7 +110,7 @@ function PublicDashboardSetting(): JSX.Element { publicDashboardResponse?.data?.timeRangeEnabled || false, ); setDefaultTimeRange( - publicDashboardResponse?.data?.defaultTimeRange || '30m', + publicDashboardResponse?.data?.defaultTimeRange || DEFAULT_TIME_RANGE, ); } }, [publicDashboardResponse]); diff --git a/frontend/src/container/Home/Home.tsx b/frontend/src/container/Home/Home.tsx index b063b68b642..a2b2867a67b 100644 --- a/frontend/src/container/Home/Home.tsx +++ b/frontend/src/container/Home/Home.tsx @@ -17,6 +17,7 @@ import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import ROUTES from 'constants/routes'; import { getMetricsListQuery } from 'container/MetricsExplorer/Summary/utils'; import { IS_SERVICE_ACCOUNTS_ENABLED } from 'container/ServiceAccountsSettings/config'; +import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants'; import { useGetMetricsList } from 'hooks/metricsExplorer/useGetMetricsList'; import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; import { useIsDarkMode } from 'hooks/useDarkMode'; @@ -81,7 +82,7 @@ export default function Home(): JSX.Element { query: initialQueriesMap[DataSource.LOGS], graphType: PANEL_TYPES.VALUE, selectedTime: 'GLOBAL_TIME', - globalSelectedInterval: '30m', + globalSelectedInterval: DEFAULT_TIME_RANGE, params: { dataSource: DataSource.LOGS, }, @@ -91,7 +92,7 @@ export default function Home(): JSX.Element { { queryKey: [ REACT_QUERY_KEY.GET_QUERY_RANGE, - '30m', + DEFAULT_TIME_RANGE, endTime || Date.now(), startTime || Date.now(), initialQueriesMap[DataSource.LOGS], @@ -106,7 +107,7 @@ export default function Home(): JSX.Element { query: initialQueriesMap[DataSource.TRACES], graphType: PANEL_TYPES.VALUE, selectedTime: 'GLOBAL_TIME', - globalSelectedInterval: '30m', + globalSelectedInterval: DEFAULT_TIME_RANGE, params: { dataSource: DataSource.TRACES, }, @@ -116,7 +117,7 @@ export default function Home(): JSX.Element { { queryKey: [ REACT_QUERY_KEY.GET_QUERY_RANGE, - '30m', + DEFAULT_TIME_RANGE, endTime || Date.now(), startTime || Date.now(), initialQueriesMap[DataSource.TRACES], diff --git a/frontend/src/container/PublicDashboardContainer/PublicDashboardContainer.tsx b/frontend/src/container/PublicDashboardContainer/PublicDashboardContainer.tsx index 67e6f9b695e..948ec35b19d 100644 --- a/frontend/src/container/PublicDashboardContainer/PublicDashboardContainer.tsx +++ b/frontend/src/container/PublicDashboardContainer/PublicDashboardContainer.tsx @@ -6,6 +6,7 @@ import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder'; import { themeColors } from 'constants/theme'; import { Card, CardContainer } from 'container/GridCardLayout/styles'; import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2'; +import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants'; import { CustomTimeType, Time, @@ -80,7 +81,7 @@ function PublicDashboardContainer({ const { widgets } = dashboard?.data || {}; const [selectedTimeRangeLabel, setSelectedTimeRangeLabel] = useState( - publicDashboard?.defaultTimeRange || '30m', + publicDashboard?.defaultTimeRange || DEFAULT_TIME_RANGE, ); const [selectedTimeRange, setSelectedTimeRange] = useState<{ @@ -88,7 +89,7 @@ function PublicDashboardContainer({ endTime: number; }>( getStartTimeAndEndTimeFromTimeRange( - publicDashboard?.defaultTimeRange || '30m', + publicDashboard?.defaultTimeRange || DEFAULT_TIME_RANGE, ), ); diff --git a/frontend/src/container/PublicDashboardContainer/__tests__/PublicDashboardContainer.test.tsx b/frontend/src/container/PublicDashboardContainer/__tests__/PublicDashboardContainer.test.tsx index 75986e7593c..162872a4734 100644 --- a/frontend/src/container/PublicDashboardContainer/__tests__/PublicDashboardContainer.test.tsx +++ b/frontend/src/container/PublicDashboardContainer/__tests__/PublicDashboardContainer.test.tsx @@ -1,5 +1,6 @@ import { Layout } from 'react-grid-layout'; import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder'; +import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { StatusCodes } from 'http-status-codes'; import { @@ -153,7 +154,6 @@ afterEach(() => { // Test constants const MOCK_PUBLIC_DASHBOARD_ID = 'test-dashboard-id'; const MOCK_PUBLIC_PATH = '/public/dashboard/test'; -const DEFAULT_TIME_RANGE = '30m'; // Use title from mock data const TEST_DASHBOARD_TITLE = publicDashboardResponse.data.dashboard.data.title; // Use widget ID from mock data diff --git a/frontend/src/container/TopNav/DateTimeSelectionV2/constants.ts b/frontend/src/container/TopNav/DateTimeSelectionV2/constants.ts index 724e185a89e..418cbe506a8 100644 --- a/frontend/src/container/TopNav/DateTimeSelectionV2/constants.ts +++ b/frontend/src/container/TopNav/DateTimeSelectionV2/constants.ts @@ -2,6 +2,8 @@ import ROUTES from 'constants/routes'; import { CustomTimeType, Option, Time, TimeFrame } from './types'; +export const DEFAULT_TIME_RANGE = '30m'; + export const Options: Option[] = [ { value: '5m', label: 'Last 5 minutes' }, { value: '15m', label: 'Last 15 minutes' }, @@ -110,7 +112,7 @@ export const convertOldTimeToNewValidCustomTimeFormat = ( return `${match[1]}${unit}` as CustomTimeType; } - return '30m'; + return DEFAULT_TIME_RANGE; }; export const getDefaultOption = (route: string): Time => { diff --git a/frontend/src/pages/AlertDetails/hooks.tsx b/frontend/src/pages/AlertDetails/hooks.tsx index 59938ee7d02..cc7e35a69d5 100644 --- a/frontend/src/pages/AlertDetails/hooks.tsx +++ b/frontend/src/pages/AlertDetails/hooks.tsx @@ -20,6 +20,7 @@ import AlertHistory from 'container/AlertHistory'; import { TIMELINE_TABLE_PAGE_SIZE } from 'container/AlertHistory/constants'; import { AlertDetailsTab, TimelineFilter } from 'container/AlertHistory/types'; import { urlKey } from 'container/AllError/utils'; +import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants'; import useAxiosError from 'hooks/useAxiosError'; import { useNotifications } from 'hooks/useNotifications'; import { useSafeNavigate } from 'hooks/useSafeNavigate'; @@ -46,8 +47,6 @@ import { PayloadProps } from 'types/api/alerts/get'; import { TagFilter } from 'types/api/queryBuilder/queryBuilderData'; import { nanoToMilli } from 'utils/timeUtils'; -const DEFAULT_TIME_RANGE = '30m'; - export const useAlertHistoryQueryParams = (): { ruleId: string | null; startTime: number; From d19592ce7b074534352247538ae7935393a08015 Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Tue, 31 Mar 2026 02:14:11 +0530 Subject: [PATCH 52/78] chore(authz): bump up openfga version (#10767) * chore(authz): bump up openfga version * chore(authz): fix tests * chore(authz): bump up openfga version * chore(authz): remove ee references --- cmd/community/server.go | 10 ++++++-- cmd/enterprise/server.go | 10 ++++++-- ee/authz/openfgaauthz/provider.go | 9 ++++--- ee/authz/openfgaserver/sqlstore.go | 32 ++++++++++++++++++++++++ ee/sqlstore/postgressqlstore/provider.go | 12 +++++++++ go.mod | 11 ++++---- go.sum | 22 ++++++++-------- pkg/authz/openfgaauthz/provider.go | 9 ++++--- pkg/authz/openfgaserver/server.go | 11 +++----- pkg/authz/openfgaserver/server_test.go | 16 ++++++------ pkg/authz/openfgaserver/sqlstore.go | 15 ++++------- pkg/signoz/signoz.go | 7 ++++-- 12 files changed, 109 insertions(+), 55 deletions(-) create mode 100644 ee/authz/openfgaserver/sqlstore.go diff --git a/cmd/community/server.go b/cmd/community/server.go index a58eb1682e9..1dd073a1a23 100644 --- a/cmd/community/server.go +++ b/cmd/community/server.go @@ -12,6 +12,7 @@ import ( "github.com/SigNoz/signoz/pkg/authz" "github.com/SigNoz/signoz/pkg/authz/openfgaauthz" "github.com/SigNoz/signoz/pkg/authz/openfgaschema" + "github.com/SigNoz/signoz/pkg/authz/openfgaserver" "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/gateway" @@ -78,8 +79,13 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error) { return signoz.NewAuthNs(ctx, providerSettings, store, licensing) }, - func(ctx context.Context, sqlstore sqlstore.SQLStore, _ licensing.Licensing, _ dashboard.Module) factory.ProviderFactory[authz.AuthZ, authz.Config] { - return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx)) + func(ctx context.Context, sqlstore sqlstore.SQLStore, _ licensing.Licensing, _ dashboard.Module) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) { + openfgaDataStore, err := openfgaserver.NewSQLStore(sqlstore) + if err != nil { + return nil, err + } + + return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore), nil }, func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing) dashboard.Module { return impldashboard.NewModule(impldashboard.NewStore(store), settings, analytics, orgGetter, queryParser) diff --git a/cmd/enterprise/server.go b/cmd/enterprise/server.go index 709b4a75aff..fc03fd7c5fe 100644 --- a/cmd/enterprise/server.go +++ b/cmd/enterprise/server.go @@ -12,6 +12,7 @@ import ( "github.com/SigNoz/signoz/ee/authn/callbackauthn/samlcallbackauthn" "github.com/SigNoz/signoz/ee/authz/openfgaauthz" "github.com/SigNoz/signoz/ee/authz/openfgaschema" + "github.com/SigNoz/signoz/ee/authz/openfgaserver" "github.com/SigNoz/signoz/ee/gateway/httpgateway" enterpriselicensing "github.com/SigNoz/signoz/ee/licensing" "github.com/SigNoz/signoz/ee/licensing/httplicensing" @@ -118,8 +119,13 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e return authNs, nil }, - func(ctx context.Context, sqlstore sqlstore.SQLStore, licensing licensing.Licensing, dashboardModule dashboard.Module) factory.ProviderFactory[authz.AuthZ, authz.Config] { - return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), licensing, dashboardModule) + func(ctx context.Context, sqlstore sqlstore.SQLStore, licensing licensing.Licensing, dashboardModule dashboard.Module) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) { + openfgaDataStore, err := openfgaserver.NewSQLStore(sqlstore) + if err != nil { + return nil, err + } + return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, licensing, dashboardModule), nil + }, func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module { return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, querier, licensing) diff --git a/ee/authz/openfgaauthz/provider.go b/ee/authz/openfgaauthz/provider.go index a3d27d88435..603e3251cf4 100644 --- a/ee/authz/openfgaauthz/provider.go +++ b/ee/authz/openfgaauthz/provider.go @@ -16,6 +16,7 @@ import ( "github.com/SigNoz/signoz/pkg/valuer" openfgav1 "github.com/openfga/api/proto/openfga/v1" openfgapkgtransformer "github.com/openfga/language/pkg/go/transformer" + "github.com/openfga/openfga/pkg/storage" ) type provider struct { @@ -26,14 +27,14 @@ type provider struct { registry []authz.RegisterTypeable } -func NewProviderFactory(sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, licensing licensing.Licensing, registry ...authz.RegisterTypeable) factory.ProviderFactory[authz.AuthZ, authz.Config] { +func NewProviderFactory(sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore, licensing licensing.Licensing, registry ...authz.RegisterTypeable) factory.ProviderFactory[authz.AuthZ, authz.Config] { return factory.NewProviderFactory(factory.MustNewName("openfga"), func(ctx context.Context, ps factory.ProviderSettings, config authz.Config) (authz.AuthZ, error) { - return newOpenfgaProvider(ctx, ps, config, sqlstore, openfgaSchema, licensing, registry) + return newOpenfgaProvider(ctx, ps, config, sqlstore, openfgaSchema, openfgaDataStore, licensing, registry) }) } -func newOpenfgaProvider(ctx context.Context, settings factory.ProviderSettings, config authz.Config, sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, licensing licensing.Licensing, registry []authz.RegisterTypeable) (authz.AuthZ, error) { - pkgOpenfgaAuthzProvider := pkgopenfgaauthz.NewProviderFactory(sqlstore, openfgaSchema) +func newOpenfgaProvider(ctx context.Context, settings factory.ProviderSettings, config authz.Config, sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore, licensing licensing.Licensing, registry []authz.RegisterTypeable) (authz.AuthZ, error) { + pkgOpenfgaAuthzProvider := pkgopenfgaauthz.NewProviderFactory(sqlstore, openfgaSchema, openfgaDataStore) pkgAuthzService, err := pkgOpenfgaAuthzProvider.New(ctx, settings, config) if err != nil { return nil, err diff --git a/ee/authz/openfgaserver/sqlstore.go b/ee/authz/openfgaserver/sqlstore.go new file mode 100644 index 00000000000..44032c6f97c --- /dev/null +++ b/ee/authz/openfgaserver/sqlstore.go @@ -0,0 +1,32 @@ +package openfgaserver + +import ( + "github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore" + "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/sqlstore" + "github.com/openfga/openfga/pkg/storage" + "github.com/openfga/openfga/pkg/storage/postgres" + "github.com/openfga/openfga/pkg/storage/sqlcommon" + "github.com/openfga/openfga/pkg/storage/sqlite" +) + +func NewSQLStore(store sqlstore.SQLStore) (storage.OpenFGADatastore, error) { + switch store.BunDB().Dialect().Name().String() { + case "sqlite": + return sqlite.NewWithDB(store.SQLDB(), &sqlcommon.Config{ + MaxTuplesPerWriteField: 100, + MaxTypesPerModelField: 100, + }) + case "pg": + pgStore, ok := store.(postgressqlstore.Pooler) + if !ok { + panic(errors.New(errors.TypeInternal, errors.CodeInternal, "postgressqlstore should implement Pooler")) + } + + return postgres.NewWithDB(pgStore.Pool(), nil, &sqlcommon.Config{ + MaxTuplesPerWriteField: 100, + MaxTypesPerModelField: 100, + }) + } + return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid store type: %s", store.BunDB().Dialect().Name().String()) +} diff --git a/ee/sqlstore/postgressqlstore/provider.go b/ee/sqlstore/postgressqlstore/provider.go index a635e851c95..3400d217018 100644 --- a/ee/sqlstore/postgressqlstore/provider.go +++ b/ee/sqlstore/postgressqlstore/provider.go @@ -14,14 +14,21 @@ import ( "github.com/uptrace/bun/dialect/pgdialect" ) +var _ Pooler = new(provider) + type provider struct { settings factory.ScopedProviderSettings sqldb *sql.DB bundb *sqlstore.BunDB + pgxPool *pgxpool.Pool dialect *dialect formatter sqlstore.SQLFormatter } +type Pooler interface { + Pool() *pgxpool.Pool +} + func NewFactory(hookFactories ...factory.ProviderFactory[sqlstore.SQLStoreHook, sqlstore.Config]) factory.ProviderFactory[sqlstore.SQLStore, sqlstore.Config] { return factory.NewProviderFactory(factory.MustNewName("postgres"), func(ctx context.Context, providerSettings factory.ProviderSettings, config sqlstore.Config) (sqlstore.SQLStore, error) { hooks := make([]sqlstore.SQLStoreHook, len(hookFactories)) @@ -62,6 +69,7 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config settings: settings, sqldb: sqldb, bundb: bunDB, + pgxPool: pool, dialect: new(dialect), formatter: newFormatter(bunDB.Dialect()), }, nil @@ -75,6 +83,10 @@ func (provider *provider) SQLDB() *sql.DB { return provider.sqldb } +func (provider *provider) Pool() *pgxpool.Pool { + return provider.pgxPool +} + func (provider *provider) Dialect() sqlstore.SQLDialect { return provider.dialect } diff --git a/go.mod b/go.mod index 7715319f20a..0bc1ddf1586 100644 --- a/go.mod +++ b/go.mod @@ -36,7 +36,7 @@ require ( github.com/open-telemetry/opamp-go v0.22.0 github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza v0.144.0 github.com/openfga/api/proto v0.0.0-20250909172242-b4b2a12f5c67 - github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20250428093642-7aeebe78bbfe + github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20251027165255-0f8f255e5f6c github.com/opentracing/opentracing-go v1.2.0 github.com/pkg/errors v0.9.1 github.com/prometheus/alertmanager v0.31.0 @@ -87,10 +87,11 @@ require ( gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/apimachinery v0.35.0 - modernc.org/sqlite v1.39.1 + modernc.org/sqlite v1.40.1 ) require ( + github.com/IBM/pgxpoolprometheus v1.1.2 // indirect github.com/aws/aws-sdk-go-v2 v1.41.1 // indirect github.com/aws/aws-sdk-go-v2/config v1.32.7 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.19.7 // indirect @@ -214,7 +215,7 @@ require ( github.com/gopherjs/gopherjs v1.17.2 // indirect github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect - github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect @@ -267,14 +268,14 @@ require ( github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.145.0 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.145.0 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.145.0 // indirect - github.com/openfga/openfga v1.10.1 + github.com/openfga/openfga v1.11.2 github.com/paulmach/orb v0.11.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pierrec/lz4/v4 v4.1.23 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect - github.com/pressly/goose/v3 v3.25.0 // indirect + github.com/pressly/goose/v3 v3.26.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/exporter-toolkit v0.15.1 // indirect github.com/prometheus/otlptranslator v1.0.0 // indirect diff --git a/go.sum b/go.sum index 31db44fd97b..9e2fc17656b 100644 --- a/go.sum +++ b/go.sum @@ -97,6 +97,8 @@ github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7Oputl github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/datadog-go v3.7.1+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/IBM/pgxpoolprometheus v1.1.2 h1:sHJwxoL5Lw4R79Zt+H4Uj1zZ4iqXJLdk7XDE7TPs97U= +github.com/IBM/pgxpoolprometheus v1.1.2/go.mod h1:+vWzISN6S9ssgurhUNmm6AlXL9XLah3TdWJktquKTR8= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= @@ -566,8 +568,8 @@ github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 h1:cLN4IBkmkYZNnk7E github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= @@ -867,10 +869,10 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/openfga/api/proto v0.0.0-20250909172242-b4b2a12f5c67 h1:58mhO5nqkdka2Mpg5mijuZOHScX7reowhzRciwjFCU8= github.com/openfga/api/proto v0.0.0-20250909172242-b4b2a12f5c67/go.mod h1:XDX4qYNBUM2Rsa2AbKPh+oocZc2zgme+EF2fFC6amVU= -github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20250428093642-7aeebe78bbfe h1:X1g0rBUMvvzMudsak/jmoEZ1NhSsp6yR0VGxWHnGMzs= -github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20250428093642-7aeebe78bbfe/go.mod h1:5Z0pbTT7Jz/oQFLfadb+C5t5NwHrduAO7j7L07Ec1GM= -github.com/openfga/openfga v1.10.1 h1:iznHh7fgmJO+XhWPOJbPUwmE2r1ruoCRgjpPiB2D164= -github.com/openfga/openfga v1.10.1/go.mod h1:LAcl94t0m+2w2cP9VWmQkwAnn0jF9tsf4Oio0n/iaAE= +github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20251027165255-0f8f255e5f6c h1:xPbHNFG8QbPr/fpL7u0MPI0x74/BCLm7Sx02btL1m5Q= +github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20251027165255-0f8f255e5f6c/go.mod h1:BG26d1Fk4GSg0wMj60TRJ6Pe4ka2WQ33akhO+mzt3t0= +github.com/openfga/openfga v1.11.2 h1:6vFZSSE0pyyt9qz320BgQLh/sHxZY5nfPOcJ3d5g8Bg= +github.com/openfga/openfga v1.11.2/go.mod h1:aCDb0gaWsU6dDAdC+zNOR2XC2W3lteGwKSkRWcSjGW8= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= @@ -909,8 +911,8 @@ github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndr github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/pressly/goose/v3 v3.25.0 h1:6WeYhMWGRCzpyd89SpODFnCBCKz41KrVbRT58nVjGng= -github.com/pressly/goose/v3 v3.25.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY= +github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM= +github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY= github.com/prometheus/alertmanager v0.31.0 h1:DQW02uIUNNiAa9AD9VA5xaFw5D+xrV+bocJc4gN9bEU= github.com/prometheus/alertmanager v0.31.0/go.mod h1:zWPQwhbLt2ybee8rL921UONeQ59Oncash+m/hGP17tU= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -1945,8 +1947,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.39.1 h1:H+/wGFzuSCIEVCvXYVHX5RQglwhMOvtHSv+VtidL2r4= -modernc.org/sqlite v1.39.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE= +modernc.org/sqlite v1.40.1 h1:VfuXcxcUWWKRBuP8+BR9L7VnmusMgBNNnBYGEe9w/iY= +modernc.org/sqlite v1.40.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= diff --git a/pkg/authz/openfgaauthz/provider.go b/pkg/authz/openfgaauthz/provider.go index 0d350c9f982..eb2306d7ee6 100644 --- a/pkg/authz/openfgaauthz/provider.go +++ b/pkg/authz/openfgaauthz/provider.go @@ -14,6 +14,7 @@ import ( "github.com/SigNoz/signoz/pkg/sqlstore" openfgav1 "github.com/openfga/api/proto/openfga/v1" openfgapkgtransformer "github.com/openfga/language/pkg/go/transformer" + "github.com/openfga/openfga/pkg/storage" ) type provider struct { @@ -21,14 +22,14 @@ type provider struct { store authtypes.RoleStore } -func NewProviderFactory(sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile) factory.ProviderFactory[authz.AuthZ, authz.Config] { +func NewProviderFactory(sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore) factory.ProviderFactory[authz.AuthZ, authz.Config] { return factory.NewProviderFactory(factory.MustNewName("openfga"), func(ctx context.Context, ps factory.ProviderSettings, config authz.Config) (authz.AuthZ, error) { - return newOpenfgaProvider(ctx, ps, config, sqlstore, openfgaSchema) + return newOpenfgaProvider(ctx, ps, config, sqlstore, openfgaSchema, openfgaDataStore) }) } -func newOpenfgaProvider(ctx context.Context, settings factory.ProviderSettings, config authz.Config, sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile) (authz.AuthZ, error) { - server, err := openfgaserver.NewOpenfgaServer(ctx, settings, config, sqlstore, openfgaSchema) +func newOpenfgaProvider(ctx context.Context, settings factory.ProviderSettings, config authz.Config, sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore) (authz.AuthZ, error) { + server, err := openfgaserver.NewOpenfgaServer(ctx, settings, config, sqlstore, openfgaSchema, openfgaDataStore) if err != nil { return nil, err } diff --git a/pkg/authz/openfgaserver/server.go b/pkg/authz/openfgaserver/server.go index b991d6241a4..538734b44ca 100644 --- a/pkg/authz/openfgaserver/server.go +++ b/pkg/authz/openfgaserver/server.go @@ -15,6 +15,7 @@ import ( openfgav1 "github.com/openfga/api/proto/openfga/v1" openfgapkgtransformer "github.com/openfga/language/pkg/go/transformer" openfgapkgserver "github.com/openfga/openfga/pkg/server" + "github.com/openfga/openfga/pkg/storage" "google.golang.org/protobuf/encoding/protojson" ) @@ -38,18 +39,12 @@ type Server struct { healthyC chan struct{} } -func NewOpenfgaServer(ctx context.Context, settings factory.ProviderSettings, config authz.Config, sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile) (*Server, error) { +func NewOpenfgaServer(ctx context.Context, settings factory.ProviderSettings, config authz.Config, sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore) (*Server, error) { scopedProviderSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/authz/openfgaauthz") - store, err := NewSQLStore(sqlstore) - if err != nil { - scopedProviderSettings.Logger().DebugContext(ctx, "failed to initialize sqlstore for authz") - return nil, err - } - // setup the openfga server opts := []openfgapkgserver.OpenFGAServiceV1Option{ - openfgapkgserver.WithDatastore(store), + openfgapkgserver.WithDatastore(openfgaDataStore), openfgapkgserver.WithLogger(NewLogger(scopedProviderSettings.Logger())), openfgapkgserver.WithContextPropagationToDatastore(true), } diff --git a/pkg/authz/openfgaserver/server_test.go b/pkg/authz/openfgaserver/server_test.go index e6975e12444..cd799f2220c 100644 --- a/pkg/authz/openfgaserver/server_test.go +++ b/pkg/authz/openfgaserver/server_test.go @@ -16,22 +16,22 @@ import ( func TestProviderStartStop(t *testing.T) { providerSettings := instrumentationtest.New().ToProviderSettings() - sqlstore := sqlstoretest.New(sqlstore.Config{Provider: "postgres"}, sqlmock.QueryMatcherRegexp) + sqlstore := sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherRegexp) + + openfgaDataStore, err := NewSQLStore(sqlstore) + require.NoError(t, err) expectedModel := `module base type user` - provider, err := NewOpenfgaServer(context.Background(), providerSettings, authz.Config{}, sqlstore, []transformer.ModuleFile{{Name: "test.fga", Contents: expectedModel}}) + provider, err := NewOpenfgaServer(context.Background(), providerSettings, authz.Config{}, sqlstore, []transformer.ModuleFile{{Name: "test.fga", Contents: expectedModel}}, openfgaDataStore) require.NoError(t, err) storeRows := sqlstore.Mock().NewRows([]string{"id", "name", "created_at", "updated_at"}).AddRow("01K3V0NTN47MPTMEV1PD5ST6ZC", "signoz", time.Now(), time.Now()) sqlstore.Mock().ExpectQuery("SELECT (.+) FROM store WHERE (.+)").WillReturnRows(storeRows) - authModelCollectionRows := sqlstore.Mock().NewRows([]string{"authorization_model_id"}).AddRow("01K44QQKXR6F729W160NFCJT58") - sqlstore.Mock().ExpectQuery("SELECT DISTINCT (.+) FROM authorization_model WHERE store (.+) ORDER BY (.+)").WillReturnRows(authModelCollectionRows) - - modelRows := sqlstore.Mock().NewRows([]string{"authorization_model_id", "schema_version", "type", "type_definition", "serialized_protobuf"}). - AddRow("01K44QQKXR6F729W160NFCJT58", "1.1", "", "", "") - sqlstore.Mock().ExpectQuery("SELECT authorization_model_id, schema_version, type, type_definition, serialized_protobuf FROM authorization_model WHERE authorization_model_id = (.+) AND store = (.+)").WithArgs("01K44QQKXR6F729W160NFCJT58", "01K3V0NTN47MPTMEV1PD5ST6ZC").WillReturnRows(modelRows) + authModelRows := sqlstore.Mock().NewRows([]string{"authorization_model_id", "schema_version", "serialized_protobuf"}). + AddRow("01K44QQKXR6F729W160NFCJT58", "1.1", []byte("")) + sqlstore.Mock().ExpectQuery("SELECT (.+) FROM authorization_model WHERE (.+)").WillReturnRows(authModelRows) sqlstore.Mock().ExpectExec("INSERT INTO authorization_model (.+) VALUES (.+)").WillReturnResult(sqlmock.NewResult(1, 1)) diff --git a/pkg/authz/openfgaserver/sqlstore.go b/pkg/authz/openfgaserver/sqlstore.go index 9842108fa1b..25c1cfc3d1b 100644 --- a/pkg/authz/openfgaserver/sqlstore.go +++ b/pkg/authz/openfgaserver/sqlstore.go @@ -4,23 +4,18 @@ import ( "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/sqlstore" "github.com/openfga/openfga/pkg/storage" - "github.com/openfga/openfga/pkg/storage/postgres" "github.com/openfga/openfga/pkg/storage/sqlcommon" "github.com/openfga/openfga/pkg/storage/sqlite" ) -func NewSQLStore(sqlstore sqlstore.SQLStore) (storage.OpenFGADatastore, error) { - switch sqlstore.BunDB().Dialect().Name().String() { +func NewSQLStore(store sqlstore.SQLStore) (storage.OpenFGADatastore, error) { + switch store.BunDB().Dialect().Name().String() { case "sqlite": - return sqlite.NewWithDB(sqlstore.SQLDB(), &sqlcommon.Config{ - MaxTuplesPerWriteField: 100, - MaxTypesPerModelField: 100, - }) - case "pg": - return postgres.NewWithDB(sqlstore.SQLDB(), nil, &sqlcommon.Config{ + return sqlite.NewWithDB(store.SQLDB(), &sqlcommon.Config{ MaxTuplesPerWriteField: 100, MaxTypesPerModelField: 100, }) + } - return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid store type: %s", sqlstore.BunDB().Dialect().Name().String()) + return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid store type: %s", store.BunDB().Dialect().Name().String()) } diff --git a/pkg/signoz/signoz.go b/pkg/signoz/signoz.go index 7cee59e6636..41e64eed28f 100644 --- a/pkg/signoz/signoz.go +++ b/pkg/signoz/signoz.go @@ -90,7 +90,7 @@ func New( sqlstoreProviderFactories factory.NamedMap[factory.ProviderFactory[sqlstore.SQLStore, sqlstore.Config]], telemetrystoreProviderFactories factory.NamedMap[factory.ProviderFactory[telemetrystore.TelemetryStore, telemetrystore.Config]], authNsCallback func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error), - authzCallback func(context.Context, sqlstore.SQLStore, licensing.Licensing, dashboard.Module) factory.ProviderFactory[authz.AuthZ, authz.Config], + authzCallback func(context.Context, sqlstore.SQLStore, licensing.Licensing, dashboard.Module) (factory.ProviderFactory[authz.AuthZ, authz.Config], error), dashboardModuleCallback func(sqlstore.SQLStore, factory.ProviderSettings, analytics.Analytics, organization.Getter, queryparser.QueryParser, querier.Querier, licensing.Licensing) dashboard.Module, gatewayProviderFactory func(licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config], querierHandlerCallback func(factory.ProviderSettings, querier.Querier, analytics.Analytics) querier.Handler, @@ -316,7 +316,10 @@ func New( dashboard := dashboardModuleCallback(sqlstore, providerSettings, analytics, orgGetter, queryParser, querier, licensing) // Initialize authz - authzProviderFactory := authzCallback(ctx, sqlstore, licensing, dashboard) + authzProviderFactory, err := authzCallback(ctx, sqlstore, licensing, dashboard) + if err != nil { + return nil, err + } authz, err := authzProviderFactory.New(ctx, providerSettings, authz.Config{}) if err != nil { return nil, err From e41d400aa0bafad5b80e8115d6aceb758dd32b17 Mon Sep 17 00:00:00 2001 From: Pandey Date: Tue, 31 Mar 2026 02:16:38 +0530 Subject: [PATCH 53/78] feat(audit): add Auditor interface and rename auditortypes to audittypes (#10766) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(audit): add Auditor interface and rename auditortypes to audittypes Add the Auditor interface in pkg/auditor/ as the contract between the HTTP handler layer and audit implementations (noop for community, OTLP for enterprise). Includes Config with buffer, batch, and flush settings following the provider pattern. Rename pkg/types/auditortypes/ to pkg/types/audittypes/ for consistency. closes SigNoz/platform-pod#1930 * refactor(audit): move endpoint config to OTLPHTTPConfig provider struct Move Endpoint out of top-level Config into a provider-specific OTLPHTTPConfig struct with full OTLP HTTP options (url_path, insecure, compression, timeout, headers, retry). Keep BufferSize, BatchSize, FlushInterval at top level as common settings across providers. closes SigNoz/platform-pod#1930 * feat(audit): add auditorbatcher for buffered event batching Add pkg/auditor/auditorbatcher/ with channel-based batching for audit events. Flushes when either batch size is reached or flush interval elapses (whichever comes first). Events are dropped when the buffer is full (fail-open). Follows the alertmanagerbatcher pattern. * refactor(audit): replace public channel with Receive method on batcher Make batchC private and expose Receive() <-chan []AuditEvent as the read-side API. Clearer contract: Add() to write, Receive() to read. * refactor(audit): rename batcher config fields to BufferSize and BatchSize Capacity → BufferSize, Size → BatchSize for clarity. * fix(audit): single-line slog call and fix log key to buffer_size * feat(audit): add OTel metrics to auditorbatcher Add telemetry via OTel MeterProvider with 4 instruments: - signoz.audit.events.emitted (counter) - signoz.audit.store.write_errors (counter, via RecordWriteError) - signoz.audit.events.dropped (counter) - signoz.audit.events.buffer_size (observable gauge) Batcher.New() now accepts metric.Meter and returns error. * refactor(audit): inject factory.ScopedProviderSettings into batcher Replace separate logger and meter params with ScopedProviderSettings, giving the batcher access to logger, meter, and tracer from one source. * feat(audit): add OTel tracing to batcher Add path Span auditorbatcher.Add with event_name attribute set at creation and audit.dropped set dynamically on buffer-full drop. * feat(audit): add export span to batcher Receive path Introduce Batch struct carrying context, events, and a trace span. Each flush starts an auditorbatcher.Export span with batch_size attribute. The consumer ends the span after export completes. * refactor(audit): replace Batch/Receive with ExportFunc callback Batcher now takes an ExportFunc at construction and manages spans internally. Removes Batch struct, Receive(), and RecordWriteError() from the public API. Span.End() is always called via defer, write errors and span status are recorded automatically on export failure. Uses errors.Attr for error logging, prefixes log keys with audit. * refactor(audit): rename auditorbatcher to auditorserver Rename package, file (batcher.go → server.go), type (Batcher → Server), and receiver (b → server) to reflect the full service role: buffering, batching, metrics, tracing, and export lifecycle management. * refactor(audit): rename telemetry to serverMetrics and document struct fields Rename type telemetry → serverMetrics and constructor newTelemetry → newServerMetrics. Add comments to all Server struct fields. * feat(audit): implement ServiceWithHealthy, fix race, add unit tests - Implement factory.ServiceWithHealthy on Server via healthyC channel - Fix data race: Start closes healthyC after goroutinesWg.Add(1), Stop waits on healthyC before closing stopC - Add 8 unit tests covering construction, start/stop lifecycle, batch-size flush, interval flush, buffer-full drop, drain on stop, export failure handling, and concurrent safety * fix(audit): fix lint issues in auditorserver and tests Use snake_case for slog keys, errors.New instead of fmt.Errorf, and check all Start/Stop return values in tests. * fix(audit): address PR review comments Use auditor:: prefix in validation error messages. Move fire-and-forget comment to the Audit method, remove interface-level comment. --- pkg/auditor/auditor.go | 12 + pkg/auditor/auditorserver/config.go | 17 ++ pkg/auditor/auditorserver/server.go | 211 +++++++++++++++++ pkg/auditor/auditorserver/server_test.go | 219 ++++++++++++++++++ pkg/auditor/auditorserver/telemetry.go | 48 ++++ pkg/auditor/config.go | 106 +++++++++ .../{auditortypes => audittypes}/action.go | 2 +- .../{auditortypes => audittypes}/category.go | 2 +- .../{auditortypes => audittypes}/event.go | 2 +- .../event_name.go | 2 +- .../{auditortypes => audittypes}/outcome.go | 2 +- .../{auditortypes => audittypes}/principal.go | 2 +- 12 files changed, 619 insertions(+), 6 deletions(-) create mode 100644 pkg/auditor/auditor.go create mode 100644 pkg/auditor/auditorserver/config.go create mode 100644 pkg/auditor/auditorserver/server.go create mode 100644 pkg/auditor/auditorserver/server_test.go create mode 100644 pkg/auditor/auditorserver/telemetry.go create mode 100644 pkg/auditor/config.go rename pkg/types/{auditortypes => audittypes}/action.go (96%) rename pkg/types/{auditortypes => audittypes}/category.go (97%) rename pkg/types/{auditortypes => audittypes}/event.go (98%) rename pkg/types/{auditortypes => audittypes}/event_name.go (97%) rename pkg/types/{auditortypes => audittypes}/outcome.go (94%) rename pkg/types/{auditortypes => audittypes}/principal.go (97%) diff --git a/pkg/auditor/auditor.go b/pkg/auditor/auditor.go new file mode 100644 index 00000000000..f4ac83553c6 --- /dev/null +++ b/pkg/auditor/auditor.go @@ -0,0 +1,12 @@ +package auditor + +import ( + "context" + + "github.com/SigNoz/signoz/pkg/types/audittypes" +) + +type Auditor interface { + // Audit emits an audit event. It is fire-and-forget: callers never block on audit outcomes. + Audit(ctx context.Context, event audittypes.AuditEvent) +} diff --git a/pkg/auditor/auditorserver/config.go b/pkg/auditor/auditorserver/config.go new file mode 100644 index 00000000000..2209c2aa8c0 --- /dev/null +++ b/pkg/auditor/auditorserver/config.go @@ -0,0 +1,17 @@ +package auditorserver + +import "time" + +type Config struct { + // BufferSize is the maximum number of events that can be buffered. + // When full, new events are dropped (fail-open). + BufferSize int + + // BatchSize is the maximum number of events per export batch. + BatchSize int + + // FlushInterval is the maximum time between flushes. + // A flush is triggered when either BatchSize events accumulate or + // FlushInterval elapses, whichever comes first. + FlushInterval time.Duration +} diff --git a/pkg/auditor/auditorserver/server.go b/pkg/auditor/auditorserver/server.go new file mode 100644 index 00000000000..e2f6965c7a1 --- /dev/null +++ b/pkg/auditor/auditorserver/server.go @@ -0,0 +1,211 @@ +package auditorserver + +import ( + "context" + "log/slog" + "sync" + "time" + + "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/factory" + "github.com/SigNoz/signoz/pkg/types/audittypes" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/trace" +) + +var _ factory.ServiceWithHealthy = (*Server)(nil) + +// ExportFunc is called by the server to export a batch of audit events. +// The context carries the active trace span for the export operation. +type ExportFunc func(ctx context.Context, events []audittypes.AuditEvent) error + +// Server buffers audit events and flushes them in batches. +// A flush is triggered when either BatchSize events accumulate or +// FlushInterval elapses, whichever comes first. +type Server struct { + // settings provides logger, meter, and tracer for instrumentation. + settings factory.ScopedProviderSettings + + // config holds buffer size, batch size, and flush interval. + config Config + + // exportFn is called with each batch of events ready for export. + exportFn ExportFunc + + // queue holds buffered events waiting to be batched. + queue []audittypes.AuditEvent + + // queueMtx guards access to queue. + queueMtx sync.Mutex + + // moreC signals the flush goroutine that new events are available. + moreC chan struct{} + + // healthyC is closed once Start has registered the flush goroutine. + // Also serves as the Healthy() signal for factory.ServiceWithHealthy. + healthyC chan struct{} + + // stopC signals the flush goroutine to drain and shut down. + stopC chan struct{} + + // goroutinesWg tracks the background flush goroutine. + goroutinesWg sync.WaitGroup + + // metrics holds OTel counters and gauges for observability. + metrics *serverMetrics +} + +func New(settings factory.ScopedProviderSettings, config Config, exportFn ExportFunc) (*Server, error) { + metrics, err := newServerMetrics(settings.Meter()) + if err != nil { + return nil, err + } + + server := &Server{ + settings: settings, + config: config, + metrics: metrics, + exportFn: exportFn, + queue: make([]audittypes.AuditEvent, 0, config.BufferSize), + moreC: make(chan struct{}, 1), + healthyC: make(chan struct{}), + stopC: make(chan struct{}), + } + + _, err = settings.Meter().RegisterCallback(func(_ context.Context, o metric.Observer) error { + o.ObserveInt64(server.metrics.bufferSize, int64(server.queueLen())) + return nil + }, server.metrics.bufferSize) + if err != nil { + return nil, err + } + + return server, nil +} + +// Start runs the background flush loop. It blocks until Stop is called. +func (server *Server) Start(ctx context.Context) error { + server.goroutinesWg.Add(1) + close(server.healthyC) + + go func() { + defer server.goroutinesWg.Done() + + ticker := time.NewTicker(server.config.FlushInterval) + defer ticker.Stop() + + for { + select { + case <-server.stopC: + server.drain(ctx) + return + case <-server.moreC: + if server.queueLen() >= server.config.BatchSize { + server.flush(ctx) + } + case <-ticker.C: + server.flush(ctx) + } + } + }() + + server.goroutinesWg.Wait() + return nil +} + +// Add enqueues an audit event for batched export. +// If the buffer is full the event is dropped and a warning is logged. +func (server *Server) Add(ctx context.Context, event audittypes.AuditEvent) { + ctx, span := server.settings.Tracer().Start(ctx, "auditorserver.Add", trace.WithAttributes(attribute.String("audit.event_name", event.EventName.String()))) + defer span.End() + + server.queueMtx.Lock() + defer server.queueMtx.Unlock() + + if len(server.queue) >= server.config.BufferSize { + server.metrics.eventsDropped.Add(ctx, 1) + span.SetAttributes(attribute.Bool("audit.dropped", true)) + server.settings.Logger().WarnContext(ctx, "audit event dropped, buffer full", slog.Int("audit_buffer_size", server.config.BufferSize)) + return + } + + server.queue = append(server.queue, event) + server.setMore() +} + +// Healthy returns a channel that is closed once the server is ready to accept events. +func (server *Server) Healthy() <-chan struct{} { + return server.healthyC +} + +// Stop signals the background loop to drain remaining events and shut down. +// It blocks until all buffered events have been exported. +func (server *Server) Stop(ctx context.Context) error { + <-server.healthyC + close(server.stopC) + server.goroutinesWg.Wait() + return nil +} + +func (server *Server) queueLen() int { + server.queueMtx.Lock() + defer server.queueMtx.Unlock() + return len(server.queue) +} + +func (server *Server) export(ctx context.Context, events []audittypes.AuditEvent) { + ctx, span := server.settings.Tracer().Start(ctx, "auditorserver.Export", trace.WithAttributes(attribute.Int("audit.batch_size", len(events)))) + defer span.End() + + server.metrics.eventsEmitted.Add(ctx, int64(len(events))) + if err := server.exportFn(ctx, events); err != nil { + server.metrics.writeErrors.Add(ctx, 1) + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + server.settings.Logger().ErrorContext(ctx, "audit batch export failed", errors.Attr(err), slog.Int("audit_batch_size", len(events))) + } +} + +func (server *Server) flush(ctx context.Context) { + events := server.next() + if len(events) == 0 { + return + } + server.export(ctx, events) +} + +func (server *Server) drain(ctx context.Context) { + for server.queueLen() > 0 { + events := server.next() + if len(events) == 0 { + return + } + server.export(ctx, events) + } +} + +func (server *Server) next() []audittypes.AuditEvent { + server.queueMtx.Lock() + defer server.queueMtx.Unlock() + + if len(server.queue) == 0 { + return nil + } + + n := min(server.config.BatchSize, len(server.queue)) + + batch := make([]audittypes.AuditEvent, n) + copy(batch, server.queue[:n]) + server.queue = server.queue[n:] + + return batch +} + +func (server *Server) setMore() { + select { + case server.moreC <- struct{}{}: + default: + } +} diff --git a/pkg/auditor/auditorserver/server_test.go b/pkg/auditor/auditorserver/server_test.go new file mode 100644 index 00000000000..5d9e3ae2472 --- /dev/null +++ b/pkg/auditor/auditorserver/server_test.go @@ -0,0 +1,219 @@ +package auditorserver + +import ( + "context" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/factory" + "github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest" + "github.com/SigNoz/signoz/pkg/types/audittypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newTestSettings() factory.ScopedProviderSettings { + return factory.NewScopedProviderSettings(instrumentationtest.New().ToProviderSettings(), "auditorserver_test") +} + +func newTestEvent(resource string, action audittypes.Action) audittypes.AuditEvent { + return audittypes.AuditEvent{ + Timestamp: time.Now(), + EventName: audittypes.NewEventName(resource, action), + ResourceName: resource, + Action: action, + Outcome: audittypes.OutcomeSuccess, + } +} + +func TestNew(t *testing.T) { + settings := newTestSettings() + config := Config{BufferSize: 10, BatchSize: 5, FlushInterval: time.Second} + + server, err := New(settings, config, func(_ context.Context, _ []audittypes.AuditEvent) error { return nil }) + require.NoError(t, err) + assert.NotNil(t, server) +} + +func TestStart_Stop(t *testing.T) { + settings := newTestSettings() + config := Config{BufferSize: 10, BatchSize: 5, FlushInterval: time.Second} + + server, err := New(settings, config, func(_ context.Context, _ []audittypes.AuditEvent) error { return nil }) + require.NoError(t, err) + + done := make(chan error, 1) + go func() { done <- server.Start(context.Background()) }() + + require.NoError(t, server.Stop(context.Background())) + + select { + case err := <-done: + assert.NoError(t, err) + case <-time.After(2 * time.Second): + assert.Fail(t, "Start did not return after Stop") + } +} + +func TestAdd_FlushesOnBatchSize(t *testing.T) { + var exported []audittypes.AuditEvent + var mu sync.Mutex + + settings := newTestSettings() + config := Config{BufferSize: 100, BatchSize: 3, FlushInterval: time.Hour} + + server, err := New(settings, config, func(_ context.Context, events []audittypes.AuditEvent) error { + mu.Lock() + exported = append(exported, events...) + mu.Unlock() + return nil + }) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go func() { _ = server.Start(ctx) }() + + for i := 0; i < 3; i++ { + server.Add(ctx, newTestEvent("dashboard", audittypes.ActionCreate)) + } + + assert.Eventually(t, func() bool { + mu.Lock() + defer mu.Unlock() + return len(exported) == 3 + }, 2*time.Second, 10*time.Millisecond) + + require.NoError(t, server.Stop(ctx)) +} + +func TestAdd_FlushesOnInterval(t *testing.T) { + var exported atomic.Int64 + + settings := newTestSettings() + config := Config{BufferSize: 100, BatchSize: 1000, FlushInterval: 50 * time.Millisecond} + + server, err := New(settings, config, func(_ context.Context, events []audittypes.AuditEvent) error { + exported.Add(int64(len(events))) + return nil + }) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go func() { _ = server.Start(ctx) }() + + server.Add(ctx, newTestEvent("user", audittypes.ActionUpdate)) + + assert.Eventually(t, func() bool { + return exported.Load() == 1 + }, 2*time.Second, 10*time.Millisecond) + + require.NoError(t, server.Stop(ctx)) +} + +func TestAdd_DropsWhenBufferFull(t *testing.T) { + settings := newTestSettings() + config := Config{BufferSize: 2, BatchSize: 100, FlushInterval: time.Hour} + + server, err := New(settings, config, func(_ context.Context, _ []audittypes.AuditEvent) error { return nil }) + require.NoError(t, err) + + ctx := context.Background() + + server.Add(ctx, newTestEvent("dashboard", audittypes.ActionCreate)) + server.Add(ctx, newTestEvent("dashboard", audittypes.ActionUpdate)) + server.Add(ctx, newTestEvent("dashboard", audittypes.ActionDelete)) + + assert.Equal(t, 2, server.queueLen()) +} + +func TestStop_DrainsRemainingEvents(t *testing.T) { + var exported atomic.Int64 + + settings := newTestSettings() + config := Config{BufferSize: 100, BatchSize: 100, FlushInterval: time.Hour} + + server, err := New(settings, config, func(_ context.Context, events []audittypes.AuditEvent) error { + exported.Add(int64(len(events))) + return nil + }) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go func() { _ = server.Start(ctx) }() + + for i := 0; i < 5; i++ { + server.Add(ctx, newTestEvent("alert-rule", audittypes.ActionCreate)) + } + + require.NoError(t, server.Stop(ctx)) + + assert.Equal(t, int64(5), exported.Load()) +} + +func TestAdd_ContinuesAfterExportFailure(t *testing.T) { + var calls atomic.Int64 + + settings := newTestSettings() + config := Config{BufferSize: 100, BatchSize: 2, FlushInterval: time.Hour} + + server, err := New(settings, config, func(_ context.Context, _ []audittypes.AuditEvent) error { + calls.Add(1) + return errors.New(errors.TypeInternal, errors.CodeInternal, "connection refused") + }) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go func() { _ = server.Start(ctx) }() + + server.Add(ctx, newTestEvent("user", audittypes.ActionDelete)) + server.Add(ctx, newTestEvent("user", audittypes.ActionDelete)) + + assert.Eventually(t, func() bool { + return calls.Load() >= 1 + }, 2*time.Second, 10*time.Millisecond) + + require.NoError(t, server.Stop(ctx)) +} + +func TestAdd_ConcurrentSafety(t *testing.T) { + var exported atomic.Int64 + + settings := newTestSettings() + config := Config{BufferSize: 1000, BatchSize: 10, FlushInterval: 50 * time.Millisecond} + + server, err := New(settings, config, func(_ context.Context, events []audittypes.AuditEvent) error { + exported.Add(int64(len(events))) + return nil + }) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go func() { _ = server.Start(ctx) }() + + var wg sync.WaitGroup + for i := 0; i < 100; i++ { + wg.Add(1) + go func() { + defer wg.Done() + server.Add(ctx, newTestEvent("dashboard", audittypes.ActionCreate)) + }() + } + wg.Wait() + + require.NoError(t, server.Stop(ctx)) + + assert.Equal(t, int64(100), exported.Load()) +} diff --git a/pkg/auditor/auditorserver/telemetry.go b/pkg/auditor/auditorserver/telemetry.go new file mode 100644 index 00000000000..a7b1b4f20ed --- /dev/null +++ b/pkg/auditor/auditorserver/telemetry.go @@ -0,0 +1,48 @@ +package auditorserver + +import ( + "github.com/SigNoz/signoz/pkg/errors" + "go.opentelemetry.io/otel/metric" +) + +type serverMetrics struct { + eventsEmitted metric.Int64Counter + writeErrors metric.Int64Counter + eventsDropped metric.Int64Counter + bufferSize metric.Int64ObservableGauge +} + +func newServerMetrics(meter metric.Meter) (*serverMetrics, error) { + var errs error + + eventsEmitted, err := meter.Int64Counter("signoz.audit.events.emitted", metric.WithDescription("Total number of audit events emitted for export.")) + if err != nil { + errs = errors.Join(errs, err) + } + + writeErrors, err := meter.Int64Counter("signoz.audit.store.write_errors", metric.WithDescription("Total number of audit store write errors during export.")) + if err != nil { + errs = errors.Join(errs, err) + } + + eventsDropped, err := meter.Int64Counter("signoz.audit.events.dropped", metric.WithDescription("Total number of audit events dropped due to a full buffer.")) + if err != nil { + errs = errors.Join(errs, err) + } + + bufferSize, err := meter.Int64ObservableGauge("signoz.audit.events.buffer_size", metric.WithDescription("Current number of audit events buffered for export.")) + if err != nil { + errs = errors.Join(errs, err) + } + + if errs != nil { + return nil, errs + } + + return &serverMetrics{ + eventsEmitted: eventsEmitted, + writeErrors: writeErrors, + eventsDropped: eventsDropped, + bufferSize: bufferSize, + }, nil +} diff --git a/pkg/auditor/config.go b/pkg/auditor/config.go new file mode 100644 index 00000000000..2c2d0055363 --- /dev/null +++ b/pkg/auditor/config.go @@ -0,0 +1,106 @@ +package auditor + +import ( + "time" + + "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/factory" +) + +var _ factory.Config = (*Config)(nil) + +type Config struct { + Provider string `mapstructure:"provider"` + + // BufferSize is the async channel capacity for audit events. + // Events are dropped when the buffer is full (fail-open). + BufferSize int `mapstructure:"buffer_size"` + + // BatchSize is the maximum number of events per export batch. + BatchSize int `mapstructure:"batch_size"` + + // FlushInterval is the maximum time between export flushes. + FlushInterval time.Duration `mapstructure:"flush_interval"` + + OTLPHTTP OTLPHTTPConfig `mapstructure:"otlphttp"` +} + +// OTLPHTTPConfig holds configuration for the OTLP HTTP exporter provider. +// Fields map to go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp options. +type OTLPHTTPConfig struct { + // Endpoint is the target host:port (without scheme or path). + Endpoint string `mapstructure:"endpoint"` + + // URLPath overrides the default URL path (/v1/logs). + URLPath string `mapstructure:"url_path"` + + // Insecure disables TLS, using HTTP instead of HTTPS. + Insecure bool `mapstructure:"insecure"` + + // Compression sets the compression strategy. Supported: "none", "gzip". + Compression string `mapstructure:"compression"` + + // Timeout is the maximum duration for an export attempt. + Timeout time.Duration `mapstructure:"timeout"` + + // Headers are additional HTTP headers sent with every export request. + Headers map[string]string `mapstructure:"headers"` + + // Retry configures exponential backoff retry policy for failed exports. + Retry RetryConfig `mapstructure:"retry"` +} + +// RetryConfig configures exponential backoff for the OTLP HTTP exporter. +type RetryConfig struct { + // Enabled controls whether retries are attempted on transient failures. + Enabled bool `mapstructure:"enabled"` + + // InitialInterval is the initial wait time before the first retry. + InitialInterval time.Duration `mapstructure:"initial_interval"` + + // MaxInterval is the upper bound on backoff interval. + MaxInterval time.Duration `mapstructure:"max_interval"` + + // MaxElapsedTime is the total maximum time spent retrying. + MaxElapsedTime time.Duration `mapstructure:"max_elapsed_time"` +} + +func newConfig() factory.Config { + return Config{ + BufferSize: 1000, + BatchSize: 100, + FlushInterval: time.Second, + OTLPHTTP: OTLPHTTPConfig{ + Endpoint: "localhost:4318", + URLPath: "/v1/logs", + Compression: "none", + Timeout: 10 * time.Second, + Retry: RetryConfig{ + Enabled: true, + InitialInterval: 5 * time.Second, + MaxInterval: 30 * time.Second, + MaxElapsedTime: time.Minute, + }, + }, + } +} + +func NewConfigFactory() factory.ConfigFactory { + return factory.NewConfigFactory(factory.MustNewName("auditor"), newConfig) +} + +func (c Config) Validate() error { + if c.BufferSize <= 0 { + return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "auditor::buffer_size must be greater than 0") + } + if c.BatchSize <= 0 { + return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "auditor::batch_size must be greater than 0") + } + if c.FlushInterval <= 0 { + return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "auditor::flush_interval must be greater than 0") + } + if c.BatchSize > c.BufferSize { + return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "auditor::batch_size must not exceed auditor::buffer_size") + } + return nil +} diff --git a/pkg/types/auditortypes/action.go b/pkg/types/audittypes/action.go similarity index 96% rename from pkg/types/auditortypes/action.go rename to pkg/types/audittypes/action.go index 370e869d03b..ca31ff16cc7 100644 --- a/pkg/types/auditortypes/action.go +++ b/pkg/types/audittypes/action.go @@ -1,4 +1,4 @@ -package auditortypes +package audittypes import "github.com/SigNoz/signoz/pkg/valuer" diff --git a/pkg/types/auditortypes/category.go b/pkg/types/audittypes/category.go similarity index 97% rename from pkg/types/auditortypes/category.go rename to pkg/types/audittypes/category.go index f583b747e52..b969b34d732 100644 --- a/pkg/types/auditortypes/category.go +++ b/pkg/types/audittypes/category.go @@ -1,4 +1,4 @@ -package auditortypes +package audittypes import "github.com/SigNoz/signoz/pkg/valuer" diff --git a/pkg/types/auditortypes/event.go b/pkg/types/audittypes/event.go similarity index 98% rename from pkg/types/auditortypes/event.go rename to pkg/types/audittypes/event.go index 3e18d951279..5f8047883cd 100644 --- a/pkg/types/auditortypes/event.go +++ b/pkg/types/audittypes/event.go @@ -1,4 +1,4 @@ -package auditortypes +package audittypes import ( "context" diff --git a/pkg/types/auditortypes/event_name.go b/pkg/types/audittypes/event_name.go similarity index 97% rename from pkg/types/auditortypes/event_name.go rename to pkg/types/audittypes/event_name.go index 9cf0ea743c6..fe91b0e09b6 100644 --- a/pkg/types/auditortypes/event_name.go +++ b/pkg/types/audittypes/event_name.go @@ -1,4 +1,4 @@ -package auditortypes +package audittypes // EventName is a typed wrapper for audit event names, ensuring not every // string qualifies as an event name. diff --git a/pkg/types/auditortypes/outcome.go b/pkg/types/audittypes/outcome.go similarity index 94% rename from pkg/types/auditortypes/outcome.go rename to pkg/types/audittypes/outcome.go index 3eeeae46e0c..86086ec718e 100644 --- a/pkg/types/auditortypes/outcome.go +++ b/pkg/types/audittypes/outcome.go @@ -1,4 +1,4 @@ -package auditortypes +package audittypes import "github.com/SigNoz/signoz/pkg/valuer" diff --git a/pkg/types/auditortypes/principal.go b/pkg/types/audittypes/principal.go similarity index 97% rename from pkg/types/auditortypes/principal.go rename to pkg/types/audittypes/principal.go index b4b7cd0d2b4..41abc7ab124 100644 --- a/pkg/types/auditortypes/principal.go +++ b/pkg/types/audittypes/principal.go @@ -1,4 +1,4 @@ -package auditortypes +package audittypes import "github.com/SigNoz/signoz/pkg/valuer" From 98f53423dcea65c1bfdd1ac07741914dccf4d010 Mon Sep 17 00:00:00 2001 From: Ayush Shukla Date: Tue, 31 Mar 2026 10:35:23 +0530 Subject: [PATCH 54/78] docs: fix typo 'versinoing' -> 'versioning' in frontend README (#10765) --- frontend/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/README.md b/frontend/README.md index ff6edd44884..1ac304587bf 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -12,7 +12,7 @@ or `docker build . -t tagname` -**Tag to remote url- Introduce versinoing later on** +**Tag to remote url- Introduce versioning later on** ``` docker tag signoz/frontend:latest 7296823551/signoz:latest From e588c57e44ea4d6dcdf14af1550dd8636f2c0dc3 Mon Sep 17 00:00:00 2001 From: Pandey Date: Tue, 31 Mar 2026 11:46:20 +0530 Subject: [PATCH 55/78] feat(audit): add noop auditor for community edition (#10769) * feat(audit): add noop auditor and embed ServiceWithHealthy in Auditor Embed factory.ServiceWithHealthy in the Auditor interface so all providers (noop and future OTLP HTTP) share a uniform lifecycle contract. Add pkg/auditor/noopauditor for the community edition that silently discards all events with zero allocations. * feat(audit): remove noopauditor test file --- pkg/auditor/auditor.go | 3 +++ pkg/auditor/noopauditor/provider.go | 42 +++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 pkg/auditor/noopauditor/provider.go diff --git a/pkg/auditor/auditor.go b/pkg/auditor/auditor.go index f4ac83553c6..50d26bd349a 100644 --- a/pkg/auditor/auditor.go +++ b/pkg/auditor/auditor.go @@ -3,10 +3,13 @@ package auditor import ( "context" + "github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/types/audittypes" ) type Auditor interface { + factory.ServiceWithHealthy + // Audit emits an audit event. It is fire-and-forget: callers never block on audit outcomes. Audit(ctx context.Context, event audittypes.AuditEvent) } diff --git a/pkg/auditor/noopauditor/provider.go b/pkg/auditor/noopauditor/provider.go new file mode 100644 index 00000000000..9c2675cb188 --- /dev/null +++ b/pkg/auditor/noopauditor/provider.go @@ -0,0 +1,42 @@ +package noopauditor + +import ( + "context" + + "github.com/SigNoz/signoz/pkg/auditor" + "github.com/SigNoz/signoz/pkg/factory" + "github.com/SigNoz/signoz/pkg/types/audittypes" +) + +type provider struct { + healthyC chan struct{} + stopC chan struct{} +} + +func NewFactory() factory.ProviderFactory[auditor.Auditor, auditor.Config] { + return factory.NewProviderFactory(factory.MustNewName("noop"), New) +} + +func New(ctx context.Context, providerSettings factory.ProviderSettings, config auditor.Config) (auditor.Auditor, error) { + return &provider{ + healthyC: make(chan struct{}), + stopC: make(chan struct{}), + }, nil +} + +func (p *provider) Start(_ context.Context) error { + close(p.healthyC) + <-p.stopC + return nil +} + +func (p *provider) Stop(_ context.Context) error { + close(p.stopC) + return nil +} + +func (p *provider) Healthy() <-chan struct{} { + return p.healthyC +} + +func (p *provider) Audit(_ context.Context, _ audittypes.AuditEvent) {} From 7b6f77bd5209b7f1a9ca53ecbe3f09efdbfbd851 Mon Sep 17 00:00:00 2001 From: Amaresh S M Date: Tue, 31 Mar 2026 12:38:41 +0530 Subject: [PATCH 56/78] fix(devenv): fix otel-collector startup failure (#10620) Co-authored-by: Nageshbansal <76246968+Nageshbansal@users.noreply.github.com> --- .devenv/docker/clickhouse/compose.yaml | 26 +++++++++---- .../docker/signoz-otel-collector/compose.yaml | 11 +++++- .../otel-collector-config.yaml | 37 ++++++++++++++----- 3 files changed, 55 insertions(+), 19 deletions(-) diff --git a/.devenv/docker/clickhouse/compose.yaml b/.devenv/docker/clickhouse/compose.yaml index e6cef658c86..51c76c0b469 100644 --- a/.devenv/docker/clickhouse/compose.yaml +++ b/.devenv/docker/clickhouse/compose.yaml @@ -27,8 +27,8 @@ services: - ${PWD}/fs/tmp/var/lib/clickhouse/user_scripts/:/var/lib/clickhouse/user_scripts/ - ${PWD}/../../../deploy/common/clickhouse/custom-function.xml:/etc/clickhouse-server/custom-function.xml ports: - - '127.0.0.1:8123:8123' - - '127.0.0.1:9000:9000' + - "127.0.0.1:8123:8123" + - "127.0.0.1:9000:9000" tty: true healthcheck: test: @@ -47,13 +47,16 @@ services: condition: service_healthy environment: - CLICKHOUSE_SKIP_USER_SETUP=1 + networks: + - default + - signoz-devenv zookeeper: image: signoz/zookeeper:3.7.1 container_name: zookeeper volumes: - ${PWD}/fs/tmp/zookeeper:/bitnami/zookeeper ports: - - '127.0.0.1:2181:2181' + - "127.0.0.1:2181:2181" environment: - ALLOW_ANONYMOUS_LOGIN=yes healthcheck: @@ -74,12 +77,19 @@ services: entrypoint: - /bin/sh command: - - -c - - | - /signoz-otel-collector migrate bootstrap && - /signoz-otel-collector migrate sync up && - /signoz-otel-collector migrate async up + - -c + - | + /signoz-otel-collector migrate bootstrap && + /signoz-otel-collector migrate sync up && + /signoz-otel-collector migrate async up depends_on: clickhouse: condition: service_healthy restart: on-failure + networks: + - default + - signoz-devenv + +networks: + signoz-devenv: + name: signoz-devenv diff --git a/.devenv/docker/signoz-otel-collector/compose.yaml b/.devenv/docker/signoz-otel-collector/compose.yaml index 46e9ba60e8d..23e67735ccd 100644 --- a/.devenv/docker/signoz-otel-collector/compose.yaml +++ b/.devenv/docker/signoz-otel-collector/compose.yaml @@ -3,7 +3,7 @@ services: image: signoz/signoz-otel-collector:v0.142.0 container_name: signoz-otel-collector-dev entrypoint: - - /bin/sh + - /bin/sh command: - -c - | @@ -34,4 +34,11 @@ services: retries: 3 restart: unless-stopped extra_hosts: - - "host.docker.internal:host-gateway" \ No newline at end of file + - "host.docker.internal:host-gateway" + networks: + - default + - signoz-devenv + +networks: + signoz-devenv: + name: signoz-devenv diff --git a/.devenv/docker/signoz-otel-collector/otel-collector-config.yaml b/.devenv/docker/signoz-otel-collector/otel-collector-config.yaml index 43a888fffb7..349f689304a 100644 --- a/.devenv/docker/signoz-otel-collector/otel-collector-config.yaml +++ b/.devenv/docker/signoz-otel-collector/otel-collector-config.yaml @@ -12,10 +12,10 @@ receivers: scrape_configs: - job_name: otel-collector static_configs: - - targets: - - localhost:8888 - labels: - job_name: otel-collector + - targets: + - localhost:8888 + labels: + job_name: otel-collector processors: batch: @@ -29,7 +29,26 @@ processors: signozspanmetrics/delta: metrics_exporter: signozclickhousemetrics metrics_flush_interval: 60s - latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ] + latency_histogram_buckets: + [ + 100us, + 1ms, + 2ms, + 6ms, + 10ms, + 50ms, + 100ms, + 250ms, + 500ms, + 1000ms, + 1400ms, + 2000ms, + 5s, + 10s, + 20s, + 40s, + 60s, + ] dimensions_cache_size: 100000 aggregation_temporality: AGGREGATION_TEMPORALITY_DELTA enable_exp_histogram: true @@ -60,13 +79,13 @@ extensions: exporters: clickhousetraces: - datasource: tcp://host.docker.internal:9000/signoz_traces + datasource: tcp://clickhouse:9000/signoz_traces low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING} use_new_schema: true signozclickhousemetrics: - dsn: tcp://host.docker.internal:9000/signoz_metrics + dsn: tcp://clickhouse:9000/signoz_metrics clickhouselogsexporter: - dsn: tcp://host.docker.internal:9000/signoz_logs + dsn: tcp://clickhouse:9000/signoz_logs timeout: 10s use_new_schema: true @@ -93,4 +112,4 @@ service: logs: receivers: [otlp] processors: [batch] - exporters: [clickhouselogsexporter] \ No newline at end of file + exporters: [clickhouselogsexporter] From b198cfc11ad0ca5393f583403abcee171e66829e Mon Sep 17 00:00:00 2001 From: Piyush Singariya Date: Tue, 31 Mar 2026 13:37:27 +0530 Subject: [PATCH 57/78] chore: enable JSON Path index in JSON Logs (#10736) * feat: enable JSON Path index * fix: contextual path index usage * test: fix unit tests --- pkg/telemetrylogs/json_condition_builder.go | 9 +++++- pkg/telemetrylogs/json_stmt_builder_test.go | 32 ++++++++++----------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/pkg/telemetrylogs/json_condition_builder.go b/pkg/telemetrylogs/json_condition_builder.go index 0665317493c..abb3459227c 100644 --- a/pkg/telemetrylogs/json_condition_builder.go +++ b/pkg/telemetrylogs/json_condition_builder.go @@ -4,6 +4,7 @@ import ( "fmt" "slices" + schemamigrator "github.com/SigNoz/signoz-otel-collector/cmd/signozschemamigrator/schema_migrator" "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/querybuilder" qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" @@ -40,8 +41,14 @@ func (c *jsonConditionBuilder) buildJSONCondition(operator qbtypes.FilterOperato } conditions = append(conditions, condition) } + baseCond := sb.Or(conditions...) - return sb.Or(conditions...), nil + // path index + if operator.AddDefaultExistsFilter() { + pathIndex := fmt.Sprintf(`has(%s, '%s')`, schemamigrator.JSONPathsIndexExpr(LogsV2BodyV2Column), c.key.ArrayParentPaths()[0]) + return sb.And(baseCond, pathIndex), nil + } + return baseCond, nil } // emitPlannedCondition handles paths with array traversal diff --git a/pkg/telemetrylogs/json_stmt_builder_test.go b/pkg/telemetrylogs/json_stmt_builder_test.go index 7aa5839beb6..950fae08ead 100644 --- a/pkg/telemetrylogs/json_stmt_builder_test.go +++ b/pkg/telemetrylogs/json_stmt_builder_test.go @@ -258,7 +258,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) { Limit: 10, }, expected: qbtypes.Statement{ - Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (dynamicElement(body_v2.`user.name`, 'String') = ?) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", + Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((dynamicElement(body_v2.`user.name`, 'String') = ?) AND has(JSONAllPaths(body_v2), 'user.name')) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", Args: []any{uint64(1747945619), uint64(1747983448), "x", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10}, }, expectedErr: nil, @@ -300,7 +300,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) { Limit: 10, }, expected: qbtypes.Statement{ - Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (arrayExists(`body_v2.education`-> (arrayExists(`body_v2.education[].awards`-> dynamicElement(`body_v2.education[].awards`.`name`, 'String') = ?, dynamicElement(`body_v2.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')) OR arrayExists(`body_v2.education[].awards`-> dynamicElement(`body_v2.education[].awards`.`name`, 'String') = ?, arrayMap(x->dynamicElement(x, 'JSON'), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education`.`awards`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", + Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_v2.education`-> (arrayExists(`body_v2.education[].awards`-> dynamicElement(`body_v2.education[].awards`.`name`, 'String') = ?, dynamicElement(`body_v2.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')) OR arrayExists(`body_v2.education[].awards`-> dynamicElement(`body_v2.education[].awards`.`name`, 'String') = ?, arrayMap(x->dynamicElement(x, 'JSON'), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education`.`awards`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", Args: []any{uint64(1747945619), uint64(1747983448), "Iron Award", "Iron Award", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10}, }, expectedErr: nil, @@ -314,7 +314,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) { Limit: 10, }, expected: qbtypes.Statement{ - Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> toFloat64(x) = ?, dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) OR (arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> toFloat64(x) = ?, arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", + Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (((arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> toFloat64(x) = ?, dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')) OR ((arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> toFloat64(x) = ?, arrayMap(x->dynamicElement(x, 'Float64'), arrayFilter(x->(dynamicType(x) = 'Float64'), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", Args: []any{uint64(1747945619), uint64(1747983448), "%1.65%", 1.65, "%1.65%", 1.65, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10}, Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)]."}, }, @@ -329,7 +329,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) { Limit: 10, }, expected: qbtypes.Statement{ - Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (arrayExists(`body_v2.education`-> LOWER(dynamicElement(`body_v2.education`.`name`, 'String')) LIKE LOWER(?), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", + Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_v2.education`-> LOWER(dynamicElement(`body_v2.education`.`name`, 'String')) LIKE LOWER(?), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", Args: []any{uint64(1747945619), uint64(1747983448), "%IIT%", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10}, }, expectedErr: nil, @@ -343,7 +343,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) { Limit: 10, }, expected: qbtypes.Statement{ - Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> x = ?, dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) OR (arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", + Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (((arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> x = ?, dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')) OR ((arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'Bool'), arrayFilter(x->(dynamicType(x) = 'Bool'), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", Args: []any{uint64(1747945619), uint64(1747983448), "%true%", true, "%true%", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10}, Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)]."}, }, @@ -358,7 +358,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) { Limit: 10, }, expected: qbtypes.Statement{ - Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> toString(x) = ?, dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) OR (arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", + Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (((arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> toString(x) = ?, dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')) OR ((arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)')))) OR arrayExists(x -> x = ?, arrayMap(x->dynamicElement(x, 'String'), arrayFilter(x->(dynamicType(x) = 'String'), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", Args: []any{uint64(1747945619), uint64(1747983448), "%passed%", "passed", "%passed%", "passed", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10}, Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)]."}, }, @@ -373,7 +373,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) { Limit: 10, }, expected: qbtypes.Statement{ - Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_v2.education`-> (arrayExists(x -> toFloat64(x) IN (?, ?), dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) OR (arrayExists(`body_v2.education`-> (arrayExists(x -> x IN (?, ?), arrayMap(x->dynamicElement(x, 'Array(Nullable(Float64))'), arrayFilter(x->(dynamicType(x) = 'Array(Nullable(Float64))'), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", + Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (((arrayExists(`body_v2.education`-> (arrayExists(x -> toFloat64(x) IN (?, ?), dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')) OR ((arrayExists(`body_v2.education`-> (arrayExists(x -> x IN (?, ?), arrayMap(x->dynamicElement(x, 'Array(Nullable(Float64))'), arrayFilter(x->(dynamicType(x) = 'Array(Nullable(Float64))'), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", Args: []any{uint64(1747945619), uint64(1747983448), 1.65, 1.99, 1.65, 1.99, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10}, Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)]."}, }, @@ -388,7 +388,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) { Limit: 10, }, expected: qbtypes.Statement{ - Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (arrayExists(`body_v2.education`-> (arrayExists(`body_v2.education[].awards`-> toFloat64(dynamicElement(`body_v2.education[].awards`.`semester`, 'Int64')) BETWEEN ? AND ?, dynamicElement(`body_v2.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')) OR arrayExists(`body_v2.education[].awards`-> toFloat64(dynamicElement(`body_v2.education[].awards`.`semester`, 'Int64')) BETWEEN ? AND ?, arrayMap(x->dynamicElement(x, 'JSON'), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education`.`awards`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", + Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_v2.education`-> (arrayExists(`body_v2.education[].awards`-> toFloat64(dynamicElement(`body_v2.education[].awards`.`semester`, 'Int64')) BETWEEN ? AND ?, dynamicElement(`body_v2.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')) OR arrayExists(`body_v2.education[].awards`-> toFloat64(dynamicElement(`body_v2.education[].awards`.`semester`, 'Int64')) BETWEEN ? AND ?, arrayMap(x->dynamicElement(x, 'JSON'), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education`.`awards`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", Args: []any{uint64(1747945619), uint64(1747983448), float64(2), float64(4), float64(2), float64(4), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10}, }, expectedErr: nil, @@ -402,7 +402,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) { Limit: 10, }, expected: qbtypes.Statement{ - Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (arrayExists(`body_v2.education`-> (arrayExists(`body_v2.education[].awards`-> toFloat64(dynamicElement(`body_v2.education[].awards`.`semester`, 'Int64')) IN (?, ?), dynamicElement(`body_v2.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')) OR arrayExists(`body_v2.education[].awards`-> toFloat64(dynamicElement(`body_v2.education[].awards`.`semester`, 'Int64')) IN (?, ?), arrayMap(x->dynamicElement(x, 'JSON'), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education`.`awards`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", + Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_v2.education`-> (arrayExists(`body_v2.education[].awards`-> toFloat64(dynamicElement(`body_v2.education[].awards`.`semester`, 'Int64')) IN (?, ?), dynamicElement(`body_v2.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')) OR arrayExists(`body_v2.education[].awards`-> toFloat64(dynamicElement(`body_v2.education[].awards`.`semester`, 'Int64')) IN (?, ?), arrayMap(x->dynamicElement(x, 'JSON'), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education`.`awards`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", Args: []any{uint64(1747945619), uint64(1747983448), float64(2), float64(4), float64(2), float64(4), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10}, }, expectedErr: nil, @@ -416,7 +416,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) { Limit: 10, }, expected: qbtypes.Statement{ - Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (arrayExists(`body_v2.education`-> (arrayExists(`body_v2.education[].awards`-> dynamicElement(`body_v2.education[].awards`.`type`, 'String') = ?, dynamicElement(`body_v2.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')) OR arrayExists(`body_v2.education[].awards`-> dynamicElement(`body_v2.education[].awards`.`type`, 'String') = ?, arrayMap(x->dynamicElement(x, 'JSON'), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education`.`awards`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", + Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_v2.education`-> (arrayExists(`body_v2.education[].awards`-> dynamicElement(`body_v2.education[].awards`.`type`, 'String') = ?, dynamicElement(`body_v2.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')) OR arrayExists(`body_v2.education[].awards`-> dynamicElement(`body_v2.education[].awards`.`type`, 'String') = ?, arrayMap(x->dynamicElement(x, 'JSON'), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education`.`awards`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", Args: []any{uint64(1747945619), uint64(1747983448), "sports", "sports", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10}, }, expectedErr: nil, @@ -430,7 +430,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) { Limit: 10, }, expected: qbtypes.Statement{ - Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_v2.interests`-> arrayExists(`body_v2.interests[].entities`-> arrayExists(`body_v2.interests[].entities[].reviews`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))')) OR arrayExists(x -> toFloat64(x) = ?, dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))'))), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_v2.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) OR (arrayExists(`body_v2.interests`-> arrayExists(`body_v2.interests[].entities`-> arrayExists(`body_v2.interests[].entities[].reviews`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))')) OR arrayExists(x -> toFloat64OrNull(x) = ?, dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))'))), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_v2.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", + Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (((arrayExists(`body_v2.interests`-> arrayExists(`body_v2.interests[].entities`-> arrayExists(`body_v2.interests[].entities[].reviews`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))')) OR arrayExists(x -> toFloat64(x) = ?, dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))'))), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_v2.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'interests')) OR ((arrayExists(`body_v2.interests`-> arrayExists(`body_v2.interests[].entities`-> arrayExists(`body_v2.interests[].entities[].reviews`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))')) OR arrayExists(x -> toFloat64OrNull(x) = ?, dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))'))), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_v2.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'interests'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", Args: []any{uint64(1747945619), uint64(1747983448), "%4%", float64(4), "%4%", float64(4), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10}, Warnings: []string{"Key `interests[].entities[].reviews[].entries[].metadata[].positions[].ratings` is ambiguous, found 2 different combinations of field context / data type: [name=interests[].entities[].reviews[].entries[].metadata[].positions[].ratings,context=body,datatype=[]int64,jsondatatype=Array(Nullable(Int64)) name=interests[].entities[].reviews[].entries[].metadata[].positions[].ratings,context=body,datatype=[]string,jsondatatype=Array(Nullable(String))]."}, }, @@ -445,7 +445,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) { Limit: 10, }, expected: qbtypes.Statement{ - Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_v2.interests`-> arrayExists(`body_v2.interests[].entities`-> arrayExists(`body_v2.interests[].entities[].reviews`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))')) OR arrayExists(x -> toString(x) = ?, dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))'))), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_v2.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) OR (arrayExists(`body_v2.interests`-> arrayExists(`body_v2.interests[].entities`-> arrayExists(`body_v2.interests[].entities[].reviews`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))')) OR arrayExists(x -> x = ?, dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))'))), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_v2.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", + Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (((arrayExists(`body_v2.interests`-> arrayExists(`body_v2.interests[].entities`-> arrayExists(`body_v2.interests[].entities[].reviews`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))')) OR arrayExists(x -> toString(x) = ?, dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))'))), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_v2.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'interests')) OR ((arrayExists(`body_v2.interests`-> arrayExists(`body_v2.interests[].entities`-> arrayExists(`body_v2.interests[].entities[].reviews`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))')) OR arrayExists(x -> x = ?, dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))'))), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_v2.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'interests'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", Args: []any{uint64(1747945619), uint64(1747983448), "%Good%", "Good", "%Good%", "Good", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10}, Warnings: []string{"Key `interests[].entities[].reviews[].entries[].metadata[].positions[].ratings` is ambiguous, found 2 different combinations of field context / data type: [name=interests[].entities[].reviews[].entries[].metadata[].positions[].ratings,context=body,datatype=[]int64,jsondatatype=Array(Nullable(Int64)) name=interests[].entities[].reviews[].entries[].metadata[].positions[].ratings,context=body,datatype=[]string,jsondatatype=Array(Nullable(String))]."}, }, @@ -460,7 +460,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) { Limit: 10, }, expected: qbtypes.Statement{ - Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (arrayExists(`body_v2.education`-> (arrayExists(`body_v2.education[].awards`-> (arrayExists(`body_v2.education[].awards[].participated`-> arrayExists(`body_v2.education[].awards[].participated[].team`-> LOWER(dynamicElement(`body_v2.education[].awards[].participated[].team`.`branch`, 'String')) LIKE LOWER(?), dynamicElement(`body_v2.education[].awards[].participated`.`team`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')) OR arrayExists(`body_v2.education[].awards[].participated`-> arrayExists(`body_v2.education[].awards[].participated[].team`-> LOWER(dynamicElement(`body_v2.education[].awards[].participated[].team`.`branch`, 'String')) LIKE LOWER(?), dynamicElement(`body_v2.education[].awards[].participated`.`team`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')), arrayMap(x->dynamicElement(x, 'JSON'), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(Dynamic)'))))), dynamicElement(`body_v2.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')) OR arrayExists(`body_v2.education[].awards`-> (arrayExists(`body_v2.education[].awards[].participated`-> arrayExists(`body_v2.education[].awards[].participated[].team`-> LOWER(dynamicElement(`body_v2.education[].awards[].participated[].team`.`branch`, 'String')) LIKE LOWER(?), dynamicElement(`body_v2.education[].awards[].participated`.`team`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=64))')), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')) OR arrayExists(`body_v2.education[].awards[].participated`-> arrayExists(`body_v2.education[].awards[].participated[].team`-> LOWER(dynamicElement(`body_v2.education[].awards[].participated[].team`.`branch`, 'String')) LIKE LOWER(?), dynamicElement(`body_v2.education[].awards[].participated`.`team`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')), arrayMap(x->dynamicElement(x, 'JSON'), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(Dynamic)'))))), arrayMap(x->dynamicElement(x, 'JSON'), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education`.`awards`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", + Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_v2.education`-> (arrayExists(`body_v2.education[].awards`-> (arrayExists(`body_v2.education[].awards[].participated`-> arrayExists(`body_v2.education[].awards[].participated[].team`-> LOWER(dynamicElement(`body_v2.education[].awards[].participated[].team`.`branch`, 'String')) LIKE LOWER(?), dynamicElement(`body_v2.education[].awards[].participated`.`team`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')) OR arrayExists(`body_v2.education[].awards[].participated`-> arrayExists(`body_v2.education[].awards[].participated[].team`-> LOWER(dynamicElement(`body_v2.education[].awards[].participated[].team`.`branch`, 'String')) LIKE LOWER(?), dynamicElement(`body_v2.education[].awards[].participated`.`team`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')), arrayMap(x->dynamicElement(x, 'JSON'), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(Dynamic)'))))), dynamicElement(`body_v2.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')) OR arrayExists(`body_v2.education[].awards`-> (arrayExists(`body_v2.education[].awards[].participated`-> arrayExists(`body_v2.education[].awards[].participated[].team`-> LOWER(dynamicElement(`body_v2.education[].awards[].participated[].team`.`branch`, 'String')) LIKE LOWER(?), dynamicElement(`body_v2.education[].awards[].participated`.`team`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=64))')), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')) OR arrayExists(`body_v2.education[].awards[].participated`-> arrayExists(`body_v2.education[].awards[].participated[].team`-> LOWER(dynamicElement(`body_v2.education[].awards[].participated[].team`.`branch`, 'String')) LIKE LOWER(?), dynamicElement(`body_v2.education[].awards[].participated`.`team`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')), arrayMap(x->dynamicElement(x, 'JSON'), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(Dynamic)'))))), arrayMap(x->dynamicElement(x, 'JSON'), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education`.`awards`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", Args: []any{uint64(1747945619), uint64(1747983448), "%Civil%", "%Civil%", "%Civil%", "%Civil%", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10}, }, expectedErr: nil, @@ -474,7 +474,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) { Limit: 10, }, expected: qbtypes.Statement{ - Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((LOWER(toString(dynamicElement(body_v2.`user.age`, 'Int64'))) LIKE LOWER(?)) OR (LOWER(dynamicElement(body_v2.`user.age`, 'String')) LIKE LOWER(?))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", + Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (((LOWER(toString(dynamicElement(body_v2.`user.age`, 'Int64'))) LIKE LOWER(?)) AND has(JSONAllPaths(body_v2), 'user.age')) OR ((LOWER(dynamicElement(body_v2.`user.age`, 'String')) LIKE LOWER(?)) AND has(JSONAllPaths(body_v2), 'user.age'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", Args: []any{uint64(1747945619), uint64(1747983448), "%25%", "%25%", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10}, Warnings: []string{"Key `user.age` is ambiguous, found 2 different combinations of field context / data type: [name=user.age,context=body,datatype=int64,jsondatatype=Int64 name=user.age,context=body,datatype=string,jsondatatype=String]."}, }, @@ -489,7 +489,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) { Limit: 10, }, expected: qbtypes.Statement{ - Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (LOWER(toString(dynamicElement(body_v2.`user.height`, 'Float64'))) LIKE LOWER(?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", + Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((LOWER(toString(dynamicElement(body_v2.`user.height`, 'Float64'))) LIKE LOWER(?)) AND has(JSONAllPaths(body_v2), 'user.height')) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", Args: []any{uint64(1747945619), uint64(1747983448), "%5.8%", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10}, }, expectedErr: nil, @@ -503,7 +503,7 @@ func TestStatementBuilderListQueryBody(t *testing.T) { Limit: 10, }, expected: qbtypes.Statement{ - Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (arrayExists(`body_v2.education`-> LOWER(toString(dynamicElement(`body_v2.education`.`year`, 'Int64'))) LIKE LOWER(?), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", + Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((arrayExists(`body_v2.education`-> LOWER(toString(dynamicElement(`body_v2.education`.`year`, 'Int64'))) LIKE LOWER(?), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", Args: []any{uint64(1747945619), uint64(1747983448), "%2020%", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10}, }, expectedErr: nil, From 13982033dcf02b437b33f57c01f7476e7a68f9c1 Mon Sep 17 00:00:00 2001 From: Tushar Vats Date: Tue, 31 Mar 2026 14:14:07 +0530 Subject: [PATCH 58/78] fix: handle empty not() expression (#10165) * fix: handle empty not() expression * fix: handle more cases * fix: short circuit conditions and updated unit tests * fix: revert commented code * fix: added more unit tests * fix: added integration tests * fix: make py-lint and make py-fmt * fix: moved from traces to logs for testing full text search * fix: simplify code * fix: added more unit tests * fix: addressed comments * fix: update comment Co-authored-by: Srikanth Chekuri * fix: update unit test Co-authored-by: Srikanth Chekuri * fix: update unit test Co-authored-by: Srikanth Chekuri * fix: instead of using true, using a skip literal * fix: unit test * fix: update integration test * fix: update unit for relevance * fix: lint error * fix: added a new literal for error condition, added more unit tests * fix: merge issues * fix: inline comments * fix: update unit tests merging from main * fix: make py-fmt and make py-lint * fix: type handling --------- Co-authored-by: Srikanth Chekuri --- pkg/querybuilder/constants.go | 10 + pkg/querybuilder/fallback_expr.go | 5 +- .../resourcefilter/condition_builder.go | 2 +- .../resourcefilter/statement_builder_test.go | 674 ++ pkg/querybuilder/where_clause_visitor.go | 152 +- pkg/querybuilder/where_clause_visitor_test.go | 1051 +- pkg/telemetrylogs/stmt_builder_test.go | 14 +- pkg/telemetrytraces/stmt_builder_test.go | 14 +- tests/integration/fixtures/querier.py | 10 + .../src/querier/08_filter_expression.py | 289 + .../testdata/filter_expressions_10000.txt | 10000 ++++++++++++++++ 11 files changed, 12153 insertions(+), 68 deletions(-) create mode 100644 pkg/querybuilder/resourcefilter/statement_builder_test.go create mode 100644 tests/integration/src/querier/08_filter_expression.py create mode 100644 tests/integration/testdata/filter_expressions_10000.txt diff --git a/pkg/querybuilder/constants.go b/pkg/querybuilder/constants.go index 0806e6fe14e..d3bd1afab54 100644 --- a/pkg/querybuilder/constants.go +++ b/pkg/querybuilder/constants.go @@ -4,6 +4,16 @@ import ( "os" ) +const ( + TrueConditionLiteral = "true" + SkipConditionLiteral = "__skip__" + ErrorConditionLiteral = "__skip_because_of_error__" +) + +var ( + SkippableConditionLiterals = []string{SkipConditionLiteral, ErrorConditionLiteral} +) + var ( BodyJSONQueryEnabled = GetOrDefaultEnv("BODY_JSON_QUERY_ENABLED", "false") == "true" ) diff --git a/pkg/querybuilder/fallback_expr.go b/pkg/querybuilder/fallback_expr.go index 74475a36fed..787d9110b44 100644 --- a/pkg/querybuilder/fallback_expr.go +++ b/pkg/querybuilder/fallback_expr.go @@ -214,7 +214,10 @@ func DataTypeCollisionHandledFieldName(key *telemetrytypes.TelemetryFieldKey, va case []any: if allFloats(v) { tblFieldName = castFloat(tblFieldName) - } else if hasString(v) { + } else { + // Any mix that is not all-floats (e.g. [bool, float64], [bool], all-strings) + // must be stringified: passing a Go bool as UInt8 against a String column + // causes ClickHouse error 386 "no supertype for String and UInt8". _, value = castString(tblFieldName), toStrings(v) } case bool: diff --git a/pkg/querybuilder/resourcefilter/condition_builder.go b/pkg/querybuilder/resourcefilter/condition_builder.go index 3233af4a557..b8476f14e5e 100644 --- a/pkg/querybuilder/resourcefilter/condition_builder.go +++ b/pkg/querybuilder/resourcefilter/condition_builder.go @@ -54,7 +54,7 @@ func (b *defaultConditionBuilder) ConditionFor( ) (string, error) { if key.FieldContext != telemetrytypes.FieldContextResource { - return "true", nil + return querybuilder.SkipConditionLiteral, nil } // except for in, not in, between, not between all other operators should have formatted value diff --git a/pkg/querybuilder/resourcefilter/statement_builder_test.go b/pkg/querybuilder/resourcefilter/statement_builder_test.go new file mode 100644 index 00000000000..719f2a669de --- /dev/null +++ b/pkg/querybuilder/resourcefilter/statement_builder_test.go @@ -0,0 +1,674 @@ +package resourcefilter + +import ( + "context" + "testing" + + "github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest" + qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" + "github.com/SigNoz/signoz/pkg/types/telemetrytypes" + "github.com/SigNoz/signoz/pkg/types/telemetrytypes/telemetrytypestest" + "github.com/stretchr/testify/require" +) + +func buildTestFieldKeyMap(signal telemetrytypes.Signal) map[string][]*telemetrytypes.TelemetryFieldKey { + keysMap := map[string][]*telemetrytypes.TelemetryFieldKey{ + "service.name": { + { + Name: "service.name", + FieldContext: telemetrytypes.FieldContextResource, + FieldDataType: telemetrytypes.FieldDataTypeString, + }, + }, + "k8s.namespace.name": { + { + Name: "k8s.namespace.name", + FieldContext: telemetrytypes.FieldContextResource, + FieldDataType: telemetrytypes.FieldDataTypeString, + }, + }, + "k8s.deployment.name": { + { + Name: "k8s.deployment.name", + FieldContext: telemetrytypes.FieldContextResource, + FieldDataType: telemetrytypes.FieldDataTypeString, + }, + }, + "env": { + { + Name: "env", + FieldContext: telemetrytypes.FieldContextResource, + FieldDataType: telemetrytypes.FieldDataTypeString, + }, + }, + "http.request.method": { + { + Name: "http.request.method", + FieldContext: telemetrytypes.FieldContextAttribute, + FieldDataType: telemetrytypes.FieldDataTypeString, + }, + }, + "deployment.environment": { + { + Name: "deployment.environment", + FieldContext: telemetrytypes.FieldContextResource, + FieldDataType: telemetrytypes.FieldDataTypeString, + }, + }, + "host.name": { + { + Name: "host.name", + FieldContext: telemetrytypes.FieldContextResource, + FieldDataType: telemetrytypes.FieldDataTypeString, + }, + }, + // Attribute fields for complex filter test + "severity_text": { + { + Name: "severity_text", + FieldContext: telemetrytypes.FieldContextAttribute, + FieldDataType: telemetrytypes.FieldDataTypeString, + }, + }, + "body": { + { + Name: "body", + FieldContext: telemetrytypes.FieldContextBody, + FieldDataType: telemetrytypes.FieldDataTypeString, + }, + }, + } + for _, keys := range keysMap { + for _, key := range keys { + key.Signal = signal + } + } + return keysMap +} + +// Test constants - values are in nanoseconds as expected by the resource filter builder +const ( + // 1747947419000000000 ns = 1747947419 seconds + testStartNs = uint64(1747947419000000000) + // 1747983448000000000 ns = 1747983448 seconds + testEndNs = uint64(1747983448000000000) + // Expected bucket start = 1747947419 - 1800 = 1747945619 + expectedBucketStart = uint64(1747945619) + // Expected bucket end = 1747983448 + expectedBucketEnd = uint64(1747983448) +) + +func TestResourceFilterStatementBuilder_Traces(t *testing.T) { + cases := []struct { + name string + query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation] + start uint64 + end uint64 + expected qbtypes.Statement + expectedErr error + }{ + { + name: "simple resource filter with service.name", + query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{ + Signal: telemetrytypes.SignalTraces, + Filter: &qbtypes.Filter{ + Expression: "service.name = 'redis-manual'", + }, + }, + start: testStartNs, + end: testEndNs, + expected: qbtypes.Statement{ + Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?", + Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", expectedBucketStart, expectedBucketEnd}, + }, + }, + { + name: "resource filter with multiple conditions AND", + query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{ + Signal: telemetrytypes.SignalTraces, + Filter: &qbtypes.Filter{ + Expression: "service.name = 'redis-manual' AND k8s.namespace.name = 'production'", + }, + }, + start: testStartNs, + end: testEndNs, + expected: qbtypes.Statement{ + Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND (simpleJSONExtractString(labels, 'k8s.namespace.name') = ? AND labels LIKE ? AND labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?", + Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", "production", "%k8s.namespace.name%", "%k8s.namespace.name\":\"production%", expectedBucketStart, expectedBucketEnd}, + }, + }, + { + name: "resource filter with OR condition - resource and attribute", + query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{ + Signal: telemetrytypes.SignalTraces, + Filter: &qbtypes.Filter{ + Expression: "service.name = 'redis-manual' OR http.request.method = 'GET'", + }, + }, + start: testStartNs, + end: testEndNs, + expected: qbtypes.Statement{ + Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?", + Args: []any{expectedBucketStart, expectedBucketEnd}, + }, + }, + { + name: "resource filter with empty filter expression", + query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{ + Signal: telemetrytypes.SignalTraces, + Filter: &qbtypes.Filter{ + Expression: "", + }, + }, + start: testStartNs, + end: testEndNs, + expected: qbtypes.Statement{ + Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?", + Args: []any{expectedBucketStart, expectedBucketEnd}, + }, + }, + { + name: "resource filter with nil filter", + query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{ + Signal: telemetrytypes.SignalTraces, + Filter: nil, + }, + start: testStartNs, + end: testEndNs, + expected: qbtypes.Statement{ + Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?", + Args: []any{expectedBucketStart, expectedBucketEnd}, + }, + }, + { + name: "resource filter with LIKE operator", + query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{ + Signal: telemetrytypes.SignalTraces, + Filter: &qbtypes.Filter{ + Expression: "service.name LIKE 'redis%'", + }, + }, + start: testStartNs, + end: testEndNs, + expected: qbtypes.Statement{ + Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (LOWER(simpleJSONExtractString(labels, 'service.name')) LIKE LOWER(?) AND labels LIKE ? AND LOWER(labels) LIKE LOWER(?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?", + Args: []any{"redis%", "%service.name%", "%service.name%redis%%", expectedBucketStart, expectedBucketEnd}, + }, + }, + { + name: "resource filter with EXISTS operator", + query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{ + Signal: telemetrytypes.SignalTraces, + Filter: &qbtypes.Filter{ + Expression: "service.name EXISTS", + }, + }, + start: testStartNs, + end: testEndNs, + expected: qbtypes.Statement{ + Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONHas(labels, 'service.name') = ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?", + Args: []any{true, "%service.name%", expectedBucketStart, expectedBucketEnd}, + }, + }, + { + name: "resource filter with NOT EXISTS operator", + query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{ + Signal: telemetrytypes.SignalTraces, + Filter: &qbtypes.Filter{ + Expression: "service.name NOT EXISTS", + }, + }, + start: testStartNs, + end: testEndNs, + expected: qbtypes.Statement{ + Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONHas(labels, 'service.name') <> ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?", + Args: []any{true, expectedBucketStart, expectedBucketEnd}, + }, + }, + { + name: "resource filter with IN operator", + query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{ + Signal: telemetrytypes.SignalTraces, + Filter: &qbtypes.Filter{ + Expression: "service.name IN ('redis', 'postgres')", + }, + }, + start: testStartNs, + end: testEndNs, + expected: qbtypes.Statement{ + Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? OR simpleJSONExtractString(labels, 'service.name') = ?) AND labels LIKE ? AND (labels LIKE ? OR labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?", + Args: []any{"redis", "postgres", "%service.name%", "%service.name\":\"redis%", "%service.name\":\"postgres%", expectedBucketStart, expectedBucketEnd}, + }, + }, + { + name: "resource filter with NOT IN operator", + query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{ + Signal: telemetrytypes.SignalTraces, + Filter: &qbtypes.Filter{ + Expression: "service.name NOT IN ('redis', 'postgres')", + }, + }, + start: testStartNs, + end: testEndNs, + expected: qbtypes.Statement{ + Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE ((simpleJSONExtractString(labels, 'service.name') <> ? AND simpleJSONExtractString(labels, 'service.name') <> ?) AND (labels NOT LIKE ? AND labels NOT LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?", + Args: []any{"redis", "postgres", "%service.name\":\"redis%", "%service.name\":\"postgres%", expectedBucketStart, expectedBucketEnd}, + }, + }, + { + name: "resource filter with CONTAINS operator", + query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{ + Signal: telemetrytypes.SignalTraces, + Filter: &qbtypes.Filter{ + Expression: "service.name CONTAINS 'redis'", + }, + }, + start: testStartNs, + end: testEndNs, + expected: qbtypes.Statement{ + Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (LOWER(simpleJSONExtractString(labels, 'service.name')) LIKE LOWER(?) AND labels LIKE ? AND LOWER(labels) LIKE LOWER(?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?", + Args: []any{"%redis%", "%service.name%", "%service.name%redis%", expectedBucketStart, expectedBucketEnd}, + }, + }, + { + name: "resource filter with REGEXP operator", + query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{ + Signal: telemetrytypes.SignalTraces, + Filter: &qbtypes.Filter{ + Expression: "service.name REGEXP 'redis.*'", + }, + }, + start: testStartNs, + end: testEndNs, + expected: qbtypes.Statement{ + Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (match(simpleJSONExtractString(labels, 'service.name'), ?) AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?", + Args: []any{"redis.*", "%service.name%", expectedBucketStart, expectedBucketEnd}, + }, + }, + { + name: "resource filter with NOT operator for equality", + query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{ + Signal: telemetrytypes.SignalTraces, + Filter: &qbtypes.Filter{ + Expression: "service.name != 'redis'", + }, + }, + start: testStartNs, + end: testEndNs, + expected: qbtypes.Statement{ + Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') <> ? AND labels NOT LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?", + Args: []any{"redis", "%service.name\":\"redis%", expectedBucketStart, expectedBucketEnd}, + }, + }, + { + name: "resource filter with attribute-only filter (should return true)", + query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{ + Signal: telemetrytypes.SignalTraces, + Filter: &qbtypes.Filter{ + Expression: "http.request.method = 'POST'", + }, + }, + start: testStartNs, + end: testEndNs, + expected: qbtypes.Statement{ + Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?", + Args: []any{expectedBucketStart, expectedBucketEnd}, + }, + }, + { + name: "resource filter with zero end time", + query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{ + Signal: telemetrytypes.SignalTraces, + Filter: &qbtypes.Filter{ + Expression: "service.name = 'redis'", + }, + }, + start: testStartNs, + end: 0, + expected: qbtypes.Statement{ + Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ?", + Args: []any{"redis", "%service.name%", "%service.name\":\"redis%", expectedBucketStart}, + }, + }, + { + name: "resource filter with NOT and inner expression", + query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{ + Signal: telemetrytypes.SignalTraces, + Filter: &qbtypes.Filter{ + Expression: "NOT (service.name = 'redis')", + }, + }, + start: testStartNs, + end: testEndNs, + expected: qbtypes.Statement{ + Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE NOT (((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?))) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?", + Args: []any{"redis", "%service.name%", "%service.name\":\"redis%", expectedBucketStart, expectedBucketEnd}, + }, + }, + { + name: "attribute filter with NOT and inner expression", + query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{ + Signal: telemetrytypes.SignalTraces, + Filter: &qbtypes.Filter{ + // http.request.method is an attribute field, not a resource field + // so the condition returns "true", and NOT should also return "true" (not "NOT (true)") + // In this system, SkipConditionLiteral means "this condition is not evaluable here" + // and the negation of "not evaluable" is also "not evaluable", + // so true is the right no-op. Returning false would incorrectly exclude all rows. + Expression: "NOT (http.request.method = 'GET')", + }, + }, + start: testStartNs, + end: testEndNs, + expected: qbtypes.Statement{ + Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?", + Args: []any{expectedBucketStart, expectedBucketEnd}, + }, + }, + } + + fm := NewFieldMapper() + cb := NewConditionBuilder(fm) + mockMetadataStore := telemetrytypestest.NewMockMetadataStore() + mockMetadataStore.KeysMap = buildTestFieldKeyMap(telemetrytypes.SignalTraces) + + builder := NewTraceResourceFilterStatementBuilder( + instrumentationtest.New().ToProviderSettings(), + fm, + cb, + mockMetadataStore, + ) + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + stmt, err := builder.Build(context.Background(), c.start, c.end, qbtypes.RequestTypeTimeSeries, c.query, nil) + + if c.expectedErr != nil { + require.Error(t, err) + require.Contains(t, err.Error(), c.expectedErr.Error()) + } else { + require.NoError(t, err) + require.Equal(t, c.expected.Query, stmt.Query) + require.Equal(t, c.expected.Args, stmt.Args) + } + }) + } +} + +func TestResourceFilterStatementBuilder_Logs(t *testing.T) { + cases := []struct { + name string + query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation] + start uint64 + end uint64 + expected qbtypes.Statement + expectedErr error + }{ + { + name: "simple resource filter with service.name for logs", + query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{ + Signal: telemetrytypes.SignalLogs, + Filter: &qbtypes.Filter{ + Expression: "service.name = 'redis-manual'", + }, + }, + start: testStartNs, + end: testEndNs, + expected: qbtypes.Statement{ + Query: "SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?", + Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", expectedBucketStart, expectedBucketEnd}, + }, + }, + { + name: "resource filter with nil filter for logs", + query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{ + Signal: telemetrytypes.SignalLogs, + Filter: nil, + }, + start: testStartNs, + end: testEndNs, + expected: qbtypes.Statement{ + Query: "SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?", + Args: []any{expectedBucketStart, expectedBucketEnd}, + }, + }, + { + name: "resource filter with empty filter expression for logs", + query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{ + Signal: telemetrytypes.SignalLogs, + Filter: &qbtypes.Filter{ + Expression: "", + }, + }, + start: testStartNs, + end: testEndNs, + expected: qbtypes.Statement{ + Query: "SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?", + Args: []any{expectedBucketStart, expectedBucketEnd}, + }, + }, + { + name: "resource filter with multiple conditions for logs", + query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{ + Signal: telemetrytypes.SignalLogs, + Filter: &qbtypes.Filter{ + Expression: "service.name = 'redis' AND k8s.namespace.name = 'default'", + }, + }, + start: testStartNs, + end: testEndNs, + expected: qbtypes.Statement{ + Query: "SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND (simpleJSONExtractString(labels, 'k8s.namespace.name') = ? AND labels LIKE ? AND labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?", + Args: []any{"redis", "%service.name%", "%service.name\":\"redis%", "default", "%k8s.namespace.name%", "%k8s.namespace.name\":\"default%", expectedBucketStart, expectedBucketEnd}, + }, + }, + { + name: "complex resource filter with mixed conditions for logs", + query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{ + Signal: telemetrytypes.SignalLogs, + Filter: &qbtypes.Filter{ + // env and k8s.deployment.name are resource fields + // severity_text is an attribute field (returns true) + // Multiple grouped conditions with attribute fields + Expression: "env = 'prod' AND k8s.deployment.name = 'prod-deployment' AND severity_text = 'ERROR' AND severity_text = 'WARN' AND (severity_text = 'INFO' AND severity_text = 'DEBUG') AND (severity_text = 'TRACE' AND severity_text = 'FATAL') AND (severity_text = 'a' AND severity_text = 'b') AND (severity_text = 'c' AND severity_text = 'd') AND (severity_text = 'e' AND severity_text = 'f')", + }, + }, + start: uint64(1769976178000000000), // These will give bucket start 1769974378 and end 1770062578 + end: uint64(1770062578000000000), + expected: qbtypes.Statement{ + Query: "SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE ((simpleJSONExtractString(labels, 'env') = ? AND labels LIKE ? AND labels LIKE ?) AND (simpleJSONExtractString(labels, 'k8s.deployment.name') = ? AND labels LIKE ? AND labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?", + Args: []any{"prod", "%env%", "%env\":\"prod%", "prod-deployment", "%k8s.deployment.name%", "%k8s.deployment.name\":\"prod-deployment%", uint64(1769974378), uint64(1770062578)}, + }, + }, + { + name: "NOT with value", + query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{ + Signal: telemetrytypes.SignalLogs, + Filter: &qbtypes.Filter{ + // using not with full text search + Expression: "NOT 'error'", + }, + }, + start: testStartNs, + end: testEndNs, + expected: qbtypes.Statement{ + Query: "SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?", + Args: []any{expectedBucketStart, expectedBucketEnd}, + }, + }, + { + name: "NOT with unknown key should not generate not()", + query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{ + Signal: telemetrytypes.SignalLogs, + Filter: &qbtypes.Filter{ + // unknown.key is not in the metadata store, so with IgnoreNotFoundKeys=true + // the condition returns empty, and NOT should also return empty (not "not()") + Expression: "NOT (unknown.key = 'value')", + }, + }, + start: testStartNs, + end: testEndNs, + expected: qbtypes.Statement{ + Query: "SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?", + Args: []any{expectedBucketStart, expectedBucketEnd}, + }, + }, + { + name: "NOT EQUAL with unknown key should not generate not()", + query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{ + Signal: telemetrytypes.SignalLogs, + Filter: &qbtypes.Filter{ + // unknown.key is not in the metadata store, so with IgnoreNotFoundKeys=true + // the condition returns empty, and NOT should also return empty (not "not()") + Expression: "not(unknown.key = 'value1' and unknown.key = 'value2')", + }, + }, + start: testStartNs, + end: testEndNs, + expected: qbtypes.Statement{ + Query: "SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?", + Args: []any{expectedBucketStart, expectedBucketEnd}, + }, + }, + { + name: "NOT with attribute field should not generate NOT (true)", + query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{ + Signal: telemetrytypes.SignalLogs, + Filter: &qbtypes.Filter{ + // http.request.method is an attribute field, not a resource field + // so the condition returns "true", and NOT should also return "true" (not "NOT (true)") + Expression: "not(http.request.method = 'POST')", + }, + }, + start: testStartNs, + end: testEndNs, + expected: qbtypes.Statement{ + Query: "SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?", + Args: []any{expectedBucketStart, expectedBucketEnd}, + }, + }, + { + name: "NOT with multiple attribute fields should not generate NOT (true and true)", + query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{ + Signal: telemetrytypes.SignalLogs, + Filter: &qbtypes.Filter{ + // http.request.method is an attribute field, not a resource field + // so the condition returns "true", and NOT should also return "true" (not "NOT (true)") + Expression: "not(http.request.method = 'POST' and module.name = 'abc')", + }, + }, + start: testStartNs, + end: testEndNs, + expected: qbtypes.Statement{ + Query: "SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?", + Args: []any{expectedBucketStart, expectedBucketEnd}, + }, + }, + { + name: "NOT with multiple attribute fields and values should not generate NOT (true and true)", + query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{ + Signal: telemetrytypes.SignalLogs, + Filter: &qbtypes.Filter{ + // http.request.method is an attribute field, not a resource field + // so the condition returns "true", and NOT should also return "true" (not "NOT (true)") + Expression: "not(http.request.method = 'POST' and (not 'error' and http.request.method = 'GET'))", + }, + }, + start: testStartNs, + end: testEndNs, + expected: qbtypes.Statement{ + Query: "SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?", + Args: []any{expectedBucketStart, expectedBucketEnd}, + }, + }, + } + + fm := NewFieldMapper() + cb := NewConditionBuilder(fm) + mockMetadataStore := telemetrytypestest.NewMockMetadataStore() + mockMetadataStore.KeysMap = buildTestFieldKeyMap(telemetrytypes.SignalLogs) + + builder := NewLogResourceFilterStatementBuilder( + instrumentationtest.New().ToProviderSettings(), + fm, + cb, + mockMetadataStore, + nil, + nil, + ) + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + stmt, err := builder.Build(context.Background(), c.start, c.end, qbtypes.RequestTypeTimeSeries, c.query, nil) + + if c.expectedErr != nil { + require.Error(t, err) + require.Contains(t, err.Error(), c.expectedErr.Error()) + } else { + require.NoError(t, err) + require.Equal(t, c.expected.Query, stmt.Query) + require.Equal(t, c.expected.Args, stmt.Args) + } + }) + } +} + +func TestResourceFilterStatementBuilder_Variables(t *testing.T) { + cases := []struct { + name string + query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation] + variables map[string]qbtypes.VariableItem + start uint64 + end uint64 + expected qbtypes.Statement + expectedErr error + }{ + { + name: "resource filter with variable substitution", + query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{ + Signal: telemetrytypes.SignalTraces, + Filter: &qbtypes.Filter{ + Expression: "service.name = $service.name", + }, + }, + variables: map[string]qbtypes.VariableItem{ + "service.name": { + Value: "redis-manual", + }, + }, + start: testStartNs, + end: testEndNs, + expected: qbtypes.Statement{ + Query: "SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?", + Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", expectedBucketStart, expectedBucketEnd}, + }, + }, + } + + fm := NewFieldMapper() + cb := NewConditionBuilder(fm) + mockMetadataStore := telemetrytypestest.NewMockMetadataStore() + mockMetadataStore.KeysMap = buildTestFieldKeyMap(telemetrytypes.SignalTraces) + + builder := NewTraceResourceFilterStatementBuilder( + instrumentationtest.New().ToProviderSettings(), + fm, + cb, + mockMetadataStore, + ) + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + stmt, err := builder.Build(context.Background(), c.start, c.end, qbtypes.RequestTypeTimeSeries, c.query, c.variables) + + if c.expectedErr != nil { + require.Error(t, err) + require.Contains(t, err.Error(), c.expectedErr.Error()) + } else { + require.NoError(t, err) + require.Equal(t, c.expected.Query, stmt.Query) + require.Equal(t, c.expected.Args, stmt.Args) + } + }) + } +} diff --git a/pkg/querybuilder/where_clause_visitor.go b/pkg/querybuilder/where_clause_visitor.go index 5519592a922..ae49af0a18e 100644 --- a/pkg/querybuilder/where_clause_visitor.go +++ b/pkg/querybuilder/where_clause_visitor.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log/slog" + "slices" "strconv" "strings" @@ -165,8 +166,11 @@ func PrepareWhereClause(query string, opts FilterExprVisitorOpts) (*PreparedWher return nil, combinedErrors.WithAdditional(visitor.errors...).WithUrl(url) } - if cond == "" { - cond = "true" + // The visitor returns exactly SkipConditionLiteral (never a substring) when + // there are no evaluable conditions; replace it with the no-op SQL literal. + // TODO(nitya): In this case we can choose to ignore resource_filter_cte + if cond == "" || cond == SkipConditionLiteral { + cond = TrueConditionLiteral } whereClause := sqlbuilder.NewWhereClause().AddWhereExpr(visitor.builder.Args, cond) @@ -178,7 +182,7 @@ func PrepareWhereClause(query string, opts FilterExprVisitorOpts) (*PreparedWher func (v *filterExpressionVisitor) Visit(tree antlr.ParseTree) any { // Handle nil nodes to prevent panic if tree == nil { - return "" + return SkipConditionLiteral } switch t := tree.(type) { @@ -217,7 +221,7 @@ func (v *filterExpressionVisitor) Visit(tree antlr.ParseTree) any { case *grammar.KeyContext: return v.VisitKey(t) default: - return "" + return ErrorConditionLiteral } } @@ -235,15 +239,20 @@ func (v *filterExpressionVisitor) VisitExpression(ctx *grammar.ExpressionContext func (v *filterExpressionVisitor) VisitOrExpression(ctx *grammar.OrExpressionContext) any { andExpressions := ctx.AllAndExpression() - andExpressionConditions := make([]string, len(andExpressions)) - for i, expr := range andExpressions { + andExpressionConditions := make([]string, 0, len(andExpressions)) + for _, expr := range andExpressions { if condExpr, ok := v.Visit(expr).(string); ok && condExpr != "" { - andExpressionConditions[i] = condExpr + // In an OR, a single unevaluable branch makes the entire expression unevaluable, + // so short-circuit immediately. + if slices.Contains(SkippableConditionLiterals, condExpr) { + return condExpr + } + andExpressionConditions = append(andExpressionConditions, condExpr) } } if len(andExpressionConditions) == 0 { - return "" + return SkipConditionLiteral } if len(andExpressionConditions) == 1 { @@ -257,15 +266,21 @@ func (v *filterExpressionVisitor) VisitOrExpression(ctx *grammar.OrExpressionCon func (v *filterExpressionVisitor) VisitAndExpression(ctx *grammar.AndExpressionContext) any { unaryExpressions := ctx.AllUnaryExpression() - unaryExpressionConditions := make([]string, len(unaryExpressions)) - for i, expr := range unaryExpressions { + unaryExpressionConditions := make([]string, 0, len(unaryExpressions)) + for _, expr := range unaryExpressions { if condExpr, ok := v.Visit(expr).(string); ok && condExpr != "" { - unaryExpressionConditions[i] = condExpr + // filter out skippable no-op conditions (e.g. non-resource attributes in resource filter) + // to avoid producing compound expressions like "(SkipConditionLiteral AND SkipConditionLiteral)" + if slices.Contains(SkippableConditionLiterals, condExpr) { + continue + } + unaryExpressionConditions = append(unaryExpressionConditions, condExpr) } } + // If there are no conditions, return SkipConditionLiteral if len(unaryExpressionConditions) == 0 { - return "" + return SkipConditionLiteral } if len(unaryExpressionConditions) == 1 { @@ -281,6 +296,10 @@ func (v *filterExpressionVisitor) VisitUnaryExpression(ctx *grammar.UnaryExpress // Check if this is a NOT expression if ctx.NOT() != nil { + // NOT (skippable) -> propagate the skippable literal unchanged + if slices.Contains(SkippableConditionLiterals, result) { + return result + } return fmt.Sprintf("NOT (%s)", result) } @@ -291,10 +310,14 @@ func (v *filterExpressionVisitor) VisitUnaryExpression(ctx *grammar.UnaryExpress func (v *filterExpressionVisitor) VisitPrimary(ctx *grammar.PrimaryContext) any { if ctx.OrExpression() != nil { // This is a parenthesized expression - if condExpr, ok := v.Visit(ctx.OrExpression()).(string); ok && condExpr != "" { - return fmt.Sprintf("(%s)", v.Visit(ctx.OrExpression()).(string)) + if condExpr, ok := v.Visit(ctx.OrExpression()).(string); ok { + // Don't wrap skippable literals in parentheses — pass them through as-is + if slices.Contains(SkippableConditionLiterals, condExpr) { + return condExpr + } + return fmt.Sprintf("(%s)", condExpr) } - return "" + return ErrorConditionLiteral } else if ctx.Comparison() != nil { return v.Visit(ctx.Comparison()) } else if ctx.FunctionCall() != nil { @@ -306,12 +329,12 @@ func (v *filterExpressionVisitor) VisitPrimary(ctx *grammar.PrimaryContext) any // Handle standalone key/value as a full text search term if ctx.GetChildCount() == 1 { if v.skipFullTextFilter { - return "" + return SkipConditionLiteral } if v.fullTextColumn == nil { v.errors = append(v.errors, "full text search is not supported") - return "" + return ErrorConditionLiteral } child := ctx.GetChild(0) var searchText string @@ -329,27 +352,27 @@ func (v *filterExpressionVisitor) VisitPrimary(ctx *grammar.PrimaryContext) any searchText = valCtx.KEY().GetText() } else { v.errors = append(v.errors, fmt.Sprintf("unsupported value type: %s", valCtx.GetText())) - return "" + return ErrorConditionLiteral } } cond, err := v.conditionBuilder.ConditionFor(context.Background(), v.startNs, v.endNs, v.fullTextColumn, qbtypes.FilterOperatorRegexp, FormatFullTextSearch(searchText), v.builder) if err != nil { v.errors = append(v.errors, fmt.Sprintf("failed to build full text search condition: %s", err.Error())) - return "" + return ErrorConditionLiteral } return cond } - return "" // Should not happen with valid input + return ErrorConditionLiteral // Should not happen with valid input } // VisitComparison handles all comparison operators func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext) any { keys := v.Visit(ctx.Key()).([]*telemetrytypes.TelemetryFieldKey) - // if key is missing and can be ignored, the condition is ignored - if len(keys) == 0 && v.ignoreNotFoundKeys { - return "" + // no keys resolved; VisitKey already recorded the error, skip this condition + if len(keys) == 0 { + return ErrorConditionLiteral } // this is used to skip the resource filtering on main table if @@ -363,7 +386,7 @@ func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext } keys = filteredKeys if len(keys) == 0 { - return "" + return SkipConditionLiteral } } @@ -378,10 +401,16 @@ func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext condition, err := v.conditionBuilder.ConditionFor(v.context, v.startNs, v.endNs, key, op, nil, v.builder) if err != nil { v.errors = append(v.errors, fmt.Sprintf("failed to build condition: %s", err.Error())) - return "" + return ErrorConditionLiteral + } + if slices.Contains(SkippableConditionLiterals, condition) { + continue } conds = append(conds, condition) } + if len(conds) == 0 { + return SkipConditionLiteral + } // if there is only one condition, return it directly, one less `()` wrapper if len(conds) == 1 { return conds[0] @@ -425,14 +454,14 @@ func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext if varItem.Type == qbtypes.DynamicVariableType { // check if it is special value to skip entire filter, if so skip it if all_, ok := varItem.Value.(string); ok && all_ == "__all__" { - return "" + return SkipConditionLiteral } } switch varValues := varItem.Value.(type) { case []any: if len(varValues) == 0 { v.errors = append(v.errors, fmt.Sprintf("malformed request payload: variable `%s` used in expression has an empty list value", strings.TrimPrefix(var_, "$"))) - return "" + return ErrorConditionLiteral } values = varValues case any: @@ -450,10 +479,16 @@ func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext for _, key := range keys { condition, err := v.conditionBuilder.ConditionFor(v.context, v.startNs, v.endNs, key, op, values, v.builder) if err != nil { - return "" + return ErrorConditionLiteral + } + if slices.Contains(SkippableConditionLiterals, condition) { + continue } conds = append(conds, condition) } + if len(conds) == 0 { + return SkipConditionLiteral + } if len(conds) == 1 { return conds[0] } @@ -472,7 +507,7 @@ func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext values := ctx.AllValue() if len(values) != 2 { - return "" + return SkipConditionLiteral } value1 := v.Visit(values[0]) @@ -482,26 +517,32 @@ func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext case float64: if _, ok := value2.(float64); !ok { v.errors = append(v.errors, fmt.Sprintf("value type mismatch for key %s: expected number for both operands", keys[0].Name)) - return "" + return ErrorConditionLiteral } case string: if _, ok := value2.(string); !ok { v.errors = append(v.errors, fmt.Sprintf("value type mismatch for key %s: expected string for both operands", keys[0].Name)) - return "" + return ErrorConditionLiteral } default: v.errors = append(v.errors, fmt.Sprintf("value type mismatch for key %s: operands must be number or string", keys[0].Name)) - return "" + return ErrorConditionLiteral } var conds []string for _, key := range keys { condition, err := v.conditionBuilder.ConditionFor(v.context, v.startNs, v.endNs, key, op, []any{value1, value2}, v.builder) if err != nil { - return "" + return ErrorConditionLiteral + } + if slices.Contains(SkippableConditionLiterals, condition) { + continue } conds = append(conds, condition) } + if len(conds) == 0 { + return SkipConditionLiteral + } if len(conds) == 1 { return conds[0] } @@ -531,7 +572,7 @@ func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext case []any: if len(varValues) == 0 { v.errors = append(v.errors, fmt.Sprintf("malformed request payload: variable `%s` used in expression has an empty list value", strings.TrimPrefix(var_, "$"))) - return "" + return ErrorConditionLiteral } value = varValues[0] case any: @@ -584,10 +625,16 @@ func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext condition, err := v.conditionBuilder.ConditionFor(v.context, v.startNs, v.endNs, key, op, value, v.builder) if err != nil { v.errors = append(v.errors, fmt.Sprintf("failed to build condition: %s", err.Error())) - return "" + return ErrorConditionLiteral + } + if slices.Contains(SkippableConditionLiterals, condition) { + continue } conds = append(conds, condition) } + if len(conds) == 0 { + return SkipConditionLiteral + } if len(conds) == 1 { return conds[0] } @@ -597,7 +644,7 @@ func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext return v.builder.Or(conds...) } - return "" // Should not happen with valid input + return ErrorConditionLiteral // Should not happen with valid input } // warnIfLikeWithoutWildcards adds a guidance warning when LIKE/ILIKE is used without wildcards @@ -644,7 +691,10 @@ func (v *filterExpressionVisitor) VisitValueList(ctx *grammar.ValueListContext) // VisitFullText handles standalone quoted strings for full-text search func (v *filterExpressionVisitor) VisitFullText(ctx *grammar.FullTextContext) any { if v.skipFullTextFilter { - return "" + // A skipped FT term must be treated as TrueConditionLiteral, not "". + // Returning "" would silently drop this branch from an OR, incorrectly + // excluding rows that could match the FT condition on the real table. + return SkipConditionLiteral } var text string @@ -657,12 +707,12 @@ func (v *filterExpressionVisitor) VisitFullText(ctx *grammar.FullTextContext) an if v.fullTextColumn == nil { v.errors = append(v.errors, "full text search is not supported") - return "" + return ErrorConditionLiteral } cond, err := v.conditionBuilder.ConditionFor(v.context, v.startNs, v.endNs, v.fullTextColumn, qbtypes.FilterOperatorRegexp, FormatFullTextSearch(text), v.builder) if err != nil { v.errors = append(v.errors, fmt.Sprintf("failed to build full text search condition: %s", err.Error())) - return "" + return ErrorConditionLiteral } return cond @@ -671,7 +721,7 @@ func (v *filterExpressionVisitor) VisitFullText(ctx *grammar.FullTextContext) an // VisitFunctionCall handles function calls like has(), hasAny(), etc. func (v *filterExpressionVisitor) VisitFunctionCall(ctx *grammar.FunctionCallContext) any { if v.skipFunctionCalls { - return "" + return SkipConditionLiteral } // Get function name based on which token is present @@ -687,19 +737,19 @@ func (v *filterExpressionVisitor) VisitFunctionCall(ctx *grammar.FunctionCallCon } else { // Default fallback v.errors = append(v.errors, fmt.Sprintf("unknown function `%s`", ctx.GetText())) - return "" + return ErrorConditionLiteral } params := v.Visit(ctx.FunctionParamList()).([]any) if len(params) < 2 { v.errors = append(v.errors, fmt.Sprintf("function `%s` expects key and value parameters", functionName)) - return "" + return ErrorConditionLiteral } keys, ok := params[0].([]*telemetrytypes.TelemetryFieldKey) if !ok { v.errors = append(v.errors, fmt.Sprintf("function `%s` expects key parameter to be a field key", functionName)) - return "" + return ErrorConditionLiteral } // filter arrays from keys @@ -712,7 +762,7 @@ func (v *filterExpressionVisitor) VisitFunctionCall(ctx *grammar.FunctionCallCon } if len(filteredKeys) == 0 { v.errors = append(v.errors, fmt.Sprintf("function `%s` expects key parameter to be an array field; no array fields found", functionName)) - return "" + return ErrorConditionLiteral } keys = filteredKeys } @@ -737,7 +787,7 @@ func (v *filterExpressionVisitor) VisitFunctionCall(ctx *grammar.FunctionCallCon v.mainErrorURL = "https://signoz.io/docs/userguide/functions-reference/#hastoken-function" } v.errors = append(v.errors, fmt.Sprintf("function `%s` expects value parameter to be a string", functionName)) - return "" + return ErrorConditionLiteral } conds = append(conds, fmt.Sprintf("hasToken(LOWER(%s), LOWER(%s))", key.Name, v.builder.Var(value[0]))) } else { @@ -748,7 +798,7 @@ func (v *filterExpressionVisitor) VisitFunctionCall(ctx *grammar.FunctionCallCon fieldName, err = v.fieldMapper.FieldFor(v.context, v.startNs, v.endNs, key) if err != nil { v.errors = append(v.errors, fmt.Sprintf("failed to get field name for key %s: %s", key.Name, err.Error())) - return "" + return ErrorConditionLiteral } } else { fieldName, _ = v.jsonKeyToKey(v.context, key, qbtypes.FilterOperatorUnknown, value) @@ -759,7 +809,7 @@ func (v *filterExpressionVisitor) VisitFunctionCall(ctx *grammar.FunctionCallCon v.mainErrorURL = "https://signoz.io/docs/userguide/search-troubleshooting/#function-supports-only-body-json-search" } v.errors = append(v.errors, fmt.Sprintf("function `%s` supports only body JSON search", functionName)) - return "" + return ErrorConditionLiteral } var cond string @@ -804,7 +854,7 @@ func (v *filterExpressionVisitor) VisitFunctionParam(ctx *grammar.FunctionParamC return v.Visit(ctx.Array()) } - return "" // Should not happen with valid input + return ErrorConditionLiteral // Should not happen with valid input } // VisitArray handles array literals @@ -822,13 +872,13 @@ func (v *filterExpressionVisitor) VisitValue(ctx *grammar.ValueContext) any { number, err := strconv.ParseFloat(ctx.NUMBER().GetText(), 64) if err != nil { v.errors = append(v.errors, fmt.Sprintf("failed to parse number %s", ctx.NUMBER().GetText())) - return "" + return ErrorConditionLiteral } return number } else if ctx.BOOL() != nil { // Convert to ClickHouse boolean literal boolText := strings.ToLower(ctx.BOOL().GetText()) - return boolText == "true" + return boolText == TrueConditionLiteral } else if ctx.KEY() != nil { // Why do we have a KEY context here? // When the user writes an expression like `service.name=redis` @@ -837,7 +887,7 @@ func (v *filterExpressionVisitor) VisitValue(ctx *grammar.ValueContext) any { return ctx.KEY().GetText() } - return "" // Should not happen with valid input + return ErrorConditionLiteral // Should not happen with valid input } // VisitKey handles field/column references diff --git a/pkg/querybuilder/where_clause_visitor_test.go b/pkg/querybuilder/where_clause_visitor_test.go index 3448b970c8c..5c9e4faf858 100644 --- a/pkg/querybuilder/where_clause_visitor_test.go +++ b/pkg/querybuilder/where_clause_visitor_test.go @@ -2,6 +2,7 @@ package querybuilder import ( "context" + "fmt" "log/slog" "strings" "testing" @@ -11,6 +12,7 @@ import ( "github.com/SigNoz/signoz/pkg/types/telemetrytypes" "github.com/antlr4-go/antlr/v4" sqlbuilder "github.com/huandu/go-sqlbuilder" + "github.com/stretchr/testify/assert" ) // TestPrepareWhereClause_EmptyVariableList ensures PrepareWhereClause errors when a variable has an empty list value @@ -43,7 +45,7 @@ func TestPrepareWhereClause_EmptyVariableList(t *testing.T) { } keys := map[string][]*telemetrytypes.TelemetryFieldKey{ - "service": []*telemetrytypes.TelemetryFieldKey{ + "service": { { Name: "service", Signal: telemetrytypes.SignalLogs, @@ -661,3 +663,1050 @@ func TestVisitKey(t *testing.T) { }) } } + +// --------------------------------------------------------------------------- +// TestVisitComparison +// --------------------------------------------------------------------------- +// +// This suite exercises the visitor with two different configurations +// side-by-side for each expression, asserting both expected outputs: +// +// resourceConditionBuilder (wantRSB) — returns TrueConditionLiteral for +// non-resource keys and "{name}_cond" for resource keys (x, y, z). +// Opts: SkipFullTextFilter:true, SkipFunctionCalls:true, IgnoreNotFoundKeys:true. +// +// conditionBuilder (wantSB) — returns "{name}_cond" for every key regardless +// of FieldContext. Opts: SkipFullTextFilter:false, SkipFunctionCalls:false, +// IgnoreNotFoundKeys:false, FullTextColumn:bodyCol. +// +// Key behavioral rules: +// +// resourceConditionBuilder: +// attr key (a,b,c) → SkipConditionLiteral (no resource match) +// resource key (x,y,z) → "{name}_cond" +// full-text / function call → SkipConditionLiteral (skipped) +// unknown key → SkipConditionLiteral (ignored; IgnoreNotFoundKeys=true) +// +// conditionBuilder: +// any key (a,b,c,x,y,z) → "{name}_cond" +// full-text ('hello') → "body_cond" (bodyCol.Name="body") +// function call (non-body) → error (only body JSON search supported) +// unknown key → error (IgnoreNotFoundKeys=false) +// +// __all__ variable (IN/NOT IN $service where service="__all__"): +// → SkipConditionLiteral in both configurations +// (PrepareWhereClause converts the final SkipConditionLiteral to TrueConditionLiteral) +// +// SkipConditionLiteral propagation rules (both configs): +// • In AND: filtered out as no-op; if ALL branches are SkipConditionLiteral +// → AND returns SkipConditionLiteral which propagates upward +// • In OR: short-circuits the entire OR immediately (returns SkipConditionLiteral) +// • NOT(SkipConditionLiteral) → SkipConditionLiteral (guard in VisitUnaryExpression) +// • PrepareWhereClause converts a top-level SkipConditionLiteral to TrueConditionLiteral ("WHERE true") +// +// Test cases with wantErrSB=true use PrepareWhereClause directly to verify +// that SB returns an error (instead of calling buildSQLOpts which fatalf's). + +// "a", "b", "c" are attribute-context fields; "x", "y", "z" are resource-context field. +var visitTestKeys = map[string][]*telemetrytypes.TelemetryFieldKey{ + "a": {{Name: "a", FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeString}}, + "b": {{Name: "b", FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeString}}, + "c": {{Name: "c", FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeString}}, + "x": {{Name: "x", FieldContext: telemetrytypes.FieldContextResource, FieldDataType: telemetrytypes.FieldDataTypeString}}, + "y": {{Name: "y", FieldContext: telemetrytypes.FieldContextResource, FieldDataType: telemetrytypes.FieldDataTypeString}}, + "z": {{Name: "z", FieldContext: telemetrytypes.FieldContextResource, FieldDataType: telemetrytypes.FieldDataTypeString}}, + "ax": {{Name: "ax", FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeNumber}, + {Name: "ax", FieldContext: telemetrytypes.FieldContextResource, FieldDataType: telemetrytypes.FieldDataTypeString}}, + "by": {{Name: "by", FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeNumber}, + {Name: "by", FieldContext: telemetrytypes.FieldContextResource, FieldDataType: telemetrytypes.FieldDataTypeString}}, + "cz": {{Name: "cz", FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeNumber}, + {Name: "cz", FieldContext: telemetrytypes.FieldContextResource, FieldDataType: telemetrytypes.FieldDataTypeString}}, +} + +type resourceConditionBuilder struct{} + +func (b *resourceConditionBuilder) ConditionFor( + _ context.Context, + _ uint64, + _ uint64, + key *telemetrytypes.TelemetryFieldKey, + _ qbtypes.FilterOperator, + _ any, + _ *sqlbuilder.SelectBuilder, +) (string, error) { + + if key.FieldContext != telemetrytypes.FieldContextResource { + return SkipConditionLiteral, nil + } + + return fmt.Sprintf("%s_cond", key.Name), nil +} + +type conditionBuilder struct{} + +func (b *conditionBuilder) ConditionFor( + _ context.Context, + _ uint64, + _ uint64, + key *telemetrytypes.TelemetryFieldKey, + _ qbtypes.FilterOperator, + _ any, + _ *sqlbuilder.SelectBuilder, +) (string, error) { + + return fmt.Sprintf("%s_cond", key.Name), nil +} + +// visitComparisonCase is a single test case for the TestVisitComparison_* family. +// Each case is run under two independent configurations: +// +// - rsbOpts (resourceConditionBuilder): skips full-text and function calls, +// ignores unknown keys, produces conditions only for resource-context keys. +// +// - sbOpts (conditionBuilder): skips resource-context keys (unless OR is present), +// evaluates full-text, errors on unknown keys. +type visitComparisonCase struct { + name string + expr string + wantRSB string // expected SQL from resourceConditionBuilder + wantSB string // expected SQL from conditionBuilder + wantErrSB bool // when true, conditionBuilder is expected to return an error + wantErrRSB bool // when true, resourceConditionBuilder is expected to return an error +} + +// visitComparisonOpts builds the two FilterExprVisitorOpts shared by all +// TestVisitComparison_* tests. +func visitComparisonOpts() (rsbOpts, sbOpts FilterExprVisitorOpts) { + allVariable := map[string]qbtypes.VariableItem{ + "service": { + Type: qbtypes.DynamicVariableType, + Value: "__all__", + }, + } + // bodyCol is the full-text column; conditionBuilder returns "body_cond" for it. + bodyCol := &telemetrytypes.TelemetryFieldKey{ + Name: "body", + FieldContext: telemetrytypes.FieldContextResource, + FieldDataType: telemetrytypes.FieldDataTypeString, + } + rsbOpts = FilterExprVisitorOpts{ + FieldKeys: visitTestKeys, + ConditionBuilder: &resourceConditionBuilder{}, + Variables: allVariable, + SkipResourceFilter: false, + SkipFullTextFilter: true, + SkipFunctionCalls: true, + IgnoreNotFoundKeys: true, + } + sbOpts = FilterExprVisitorOpts{ + FieldKeys: visitTestKeys, + ConditionBuilder: &conditionBuilder{}, + Variables: allVariable, + SkipResourceFilter: true, + SkipFullTextFilter: false, + SkipFunctionCalls: false, + IgnoreNotFoundKeys: false, + FullTextColumn: bodyCol, + } + return +} + +// TestVisitComparison_AND covers AND expressions with attribute keys (a, b, c → +// TrueConditionLiteral in RSB) and resource keys (x, y, z → "{name}_cond" in RSB). +func TestVisitComparison_AND(t *testing.T) { + rsbOpts, sbOpts := visitComparisonOpts() + tests := []visitComparisonCase{ + { + name: "single attribute key", + expr: "a = 'v'", + wantRSB: "WHERE true", + wantSB: "WHERE a_cond", + }, + { + name: "single resource key", + expr: "x = 'x'", + wantRSB: "WHERE x_cond", + wantSB: "WHERE true", + }, + { + // RSB: both attribute keys → true; AND propagates TrueConditionLiteral. + name: "two attribute keys AND", + expr: "a = 'a' AND b = 'b'", + wantRSB: "WHERE true", + wantSB: "WHERE (a_cond AND b_cond)", + }, + { + name: "two resource keys AND", + expr: "x = 'x' AND y = 'y'", + wantRSB: "WHERE (x_cond AND y_cond)", + wantSB: "WHERE true", + }, + { + // RSB: attribute → true stripped by AND; resource key survives. + name: "attribute AND resource", + expr: "a = 'a' AND x = 'x'", + wantRSB: "WHERE x_cond", + wantSB: "WHERE a_cond", + }, + { + // RSB: a, b → true stripped; x_cond remains. + name: "three mixed keys AND", + expr: "a = 'a' AND b = 'b' AND x = 'x'", + wantRSB: "WHERE x_cond", + wantSB: "WHERE (a_cond AND b_cond)", + }, + { + // RSB: a, b → true stripped; x, y survive. + name: "four mixed keys in AND", + expr: "a = 'a' AND b = 'b' AND x = 'x' AND y = 'y'", + wantRSB: "WHERE (x_cond AND y_cond)", + wantSB: "WHERE (a_cond AND b_cond)", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := PrepareWhereClause(tt.expr, rsbOpts) + assert.Equal(t, tt.wantErrRSB, err != nil, "resourceConditionBuilder: error expectation mismatch") + if err == nil { + expr, _ := result.WhereClause.Build() + assert.Equal(t, tt.wantRSB, expr, "resourceConditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantRSB, expr) + } + result, err = PrepareWhereClause(tt.expr, sbOpts) + assert.Equal(t, tt.wantErrSB, err != nil, "conditionBuilder: error expectation mismatch") + if err == nil { + expr, _ := result.WhereClause.Build() + assert.Equal(t, tt.wantSB, expr, "conditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantSB, expr) + } + }) + } +} + +// TestVisitComparison_NOT covers the NOT operator in two positions: +// - unary NOT wrapping a comparison: structural NOT emitted by the visitor. +// - NOT inside a comparison (e.g. NOT LIKE, NOT EXISTS): the inner NOT is folded +// into the operator token; conditionBuilder ignores it, so no extra NOT is emitted. +func TestVisitComparison_NOT(t *testing.T) { + rsbOpts, sbOpts := visitComparisonOpts() + tests := []visitComparisonCase{ + { + // Unary NOT on an attribute key: NOT(SkipConditionLiteral) → SkipConditionLiteral (guard). + name: "NOT attribute key", + expr: "NOT a = 'a'", + wantRSB: "WHERE true", + wantSB: "WHERE NOT (a_cond)", + }, + { + name: "NOT resource key", + expr: "NOT x = 'x'", + wantRSB: "WHERE NOT (x_cond)", + wantSB: "WHERE true", + }, + { + // RSB: NOT(SkipConditionLiteral) → SkipConditionLiteral; stripped from AND; x_cond survives. + name: "NOT attribute AND resource", + expr: "NOT a = 'a' AND x = 'x'", + wantRSB: "WHERE x_cond", + wantSB: "WHERE NOT (a_cond)", + }, + { + // NOT inside comparison (op=NotLike): conditionBuilder ignores it → same as LIKE. + name: "NOT inside LIKE comparison", + expr: "a NOT LIKE '%a%'", + wantRSB: "WHERE true", + wantSB: "WHERE a_cond", + }, + { + // Unary NOT wrapping LIKE: structural NOT emitted around a_cond. + name: "NOT at unary level wrapping LIKE", + expr: "NOT a LIKE '%a%'", + wantRSB: "WHERE true", + wantSB: "WHERE NOT (a_cond)", + }, + { + // NOT inside comparison on resource key in OR: no outer NOT, builder ignores inner op. + name: "NOT inside LIKE OR resource", + expr: "x NOT LIKE '%x%' OR y = 'y'", + wantRSB: "WHERE (x_cond OR y_cond)", + wantSB: "WHERE (x_cond OR y_cond)", + }, + { + name: "NOT at unary level wrapping LIKE OR resource", + expr: "NOT x LIKE '%x%' OR y = 'y'", + wantRSB: "WHERE (NOT (x_cond) OR y_cond)", + wantSB: "WHERE (NOT (x_cond) OR y_cond)", + }, + { + // RSB: a, b → true stripped; NOT(x_cond) remains. + name: "three NOTs in AND", + expr: "NOT a = 'a' AND NOT b = 'b' AND NOT x = 'x'", + wantRSB: "WHERE NOT (x_cond)", + wantSB: "WHERE (NOT (a_cond) AND NOT (b_cond))", + }, + { + // Unary NOT wraps a comparison that itself has NOT (op=NotLike). + // The inner NOT is an operator token; the outer NOT is structural. + name: "unary NOT wrapping comparison NOT LIKE", + expr: "NOT (a NOT LIKE '%a%')", + wantRSB: "WHERE true", + wantSB: "WHERE NOT ((a_cond))", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := PrepareWhereClause(tt.expr, rsbOpts) + assert.Equal(t, tt.wantErrRSB, err != nil, "resourceConditionBuilder: error expectation mismatch") + if err == nil { + expr, _ := result.WhereClause.Build() + assert.Equal(t, tt.wantRSB, expr, "resourceConditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantRSB, expr) + } + result, err = PrepareWhereClause(tt.expr, sbOpts) + assert.Equal(t, tt.wantErrSB, err != nil, "conditionBuilder: error expectation mismatch") + if err == nil { + expr, _ := result.WhereClause.Build() + assert.Equal(t, tt.wantSB, expr, "conditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantSB, expr) + } + }) + } +} + +// TestVisitComparison_OR covers OR expressions. PrepareWhereClause auto-resets +// SkipResourceFilter to false when an OR token is detected in the expression, +// so resource keys become visible in sbOpts for all cases in this suite. +func TestVisitComparison_OR(t *testing.T) { + rsbOpts, sbOpts := visitComparisonOpts() + tests := []visitComparisonCase{ + { + name: "resource OR resource", + expr: "x = 'x' OR y = 'y'", + wantRSB: "WHERE (x_cond OR y_cond)", + wantSB: "WHERE (x_cond OR y_cond)", + }, + { + name: "three resource keys OR", + expr: "x = 'x' OR y = 'y' OR z = 'z'", + wantRSB: "WHERE (x_cond OR y_cond OR z_cond)", + wantSB: "WHERE (x_cond OR y_cond OR z_cond)", + }, + { + // RSB: attribute → TrueConditionLiteral short-circuits OR. + name: "attribute OR resource", + expr: "a = 'a' OR x = 'x'", + wantRSB: "WHERE true", + wantSB: "WHERE (a_cond OR x_cond)", + }, + { + name: "AND sub-expression OR resource", + expr: "x = 'x' AND y = 'y' OR z = 'z'", + wantRSB: "WHERE ((x_cond AND y_cond) OR z_cond)", + wantSB: "WHERE ((x_cond AND y_cond) OR z_cond)", + }, + { + name: "parenthesized OR AND resource", + expr: "(x = 'x' OR y = 'y') AND z = 'z'", + wantRSB: "WHERE (((x_cond OR y_cond)) AND z_cond)", + wantSB: "WHERE (((x_cond OR y_cond)) AND z_cond)", + }, + { + name: "NOT resource OR resource", + expr: "NOT x = 'x' OR y = 'y'", + wantRSB: "WHERE (NOT (x_cond) OR y_cond)", + wantSB: "WHERE (NOT (x_cond) OR y_cond)", + }, + { + name: "NOT of parenthesized OR", + expr: "NOT (x = 'x' OR y = 'y')", + wantRSB: "WHERE NOT (((x_cond OR y_cond)))", + wantSB: "WHERE NOT (((x_cond OR y_cond)))", + }, + { + // RSB: left (a→true, x_cond) → x_cond; right (b→true, y_cond) → y_cond. + name: "two mixed AND groups in OR", + expr: "a = 'a' AND x = 'x' OR b = 'b' AND y = 'y'", + wantRSB: "WHERE (x_cond OR y_cond)", + wantSB: "WHERE ((a_cond AND x_cond) OR (b_cond AND y_cond))", + }, + { + // RSB: NOT(a→SkipConditionLiteral) → SkipConditionLiteral → OR short-circuits. + name: "NOT attr OR resource with OR override", + expr: "NOT a = 'a' OR x = 'x'", + wantRSB: "WHERE true", + wantSB: "WHERE (NOT (a_cond) OR x_cond)", + }, + { + // RSB: a → TrueConditionLiteral → OR short-circuits. + name: "all attribute keys OR", + expr: "a = 'a' OR b = 'b' OR c = 'c'", + wantRSB: "WHERE true", + wantSB: "WHERE (a_cond OR b_cond OR c_cond)", + }, + { + // RSB: a→SkipConditionLiteral → OR short-circuits; paren passes through; NOT(SkipConditionLiteral) → SkipConditionLiteral. + name: "NOT of three-way OR", + expr: "NOT (a = 'a' OR b = 'b' OR x = 'x')", + wantRSB: "WHERE true", + wantSB: "WHERE NOT (((a_cond OR b_cond OR x_cond)))", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := PrepareWhereClause(tt.expr, rsbOpts) + assert.Equal(t, tt.wantErrRSB, err != nil, "resourceConditionBuilder: error expectation mismatch") + if err == nil { + expr, _ := result.WhereClause.Build() + assert.Equal(t, tt.wantRSB, expr, "resourceConditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantRSB, expr) + } + result, err = PrepareWhereClause(tt.expr, sbOpts) + assert.Equal(t, tt.wantErrSB, err != nil, "conditionBuilder: error expectation mismatch") + if err == nil { + expr, _ := result.WhereClause.Build() + assert.Equal(t, tt.wantSB, expr, "conditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantSB, expr) + } + }) + } +} + +// TestVisitComparison_Precedence covers AND/OR/NOT operator precedence +// (AND binds tighter than OR; NOT binds tightest). +func TestVisitComparison_Precedence(t *testing.T) { + rsbOpts, sbOpts := visitComparisonOpts() + tests := []visitComparisonCase{ + { + // a→true short-circuits OR. + name: "attr OR attr OR resource", + expr: "a = 'a' OR b = 'b' OR x = 'x'", + wantRSB: "WHERE true", + wantSB: "WHERE (a_cond OR b_cond OR x_cond)", + }, + { + // AND before OR: (a AND b)→true short-circuits OR. + name: "attr AND attr OR resource", + expr: "a = 'a' AND b = 'b' OR x = 'x'", + wantRSB: "WHERE true", + wantSB: "WHERE ((a_cond AND b_cond) OR x_cond)", + }, + { + // AND tighter: a as own OR branch; (b AND x) as second. + name: "attr OR attr AND resource", + expr: "a = 'a' OR b = 'b' AND x = 'x'", + wantRSB: "WHERE true", + wantSB: "WHERE (a_cond OR (b_cond AND x_cond))", + }, + { + // Left AND group (a,b)→true short-circuits OR. + name: "two AND groups OR", + expr: "a = 'a' AND b = 'b' OR x = 'x' AND y = 'y'", + wantRSB: "WHERE true", + wantSB: "WHERE ((a_cond AND b_cond) OR (x_cond AND y_cond))", + }, + { + // RSB: NOT(a→SkipConditionLiteral)→SkipConditionLiteral stripped from AND; NOT(x_cond) remains. + name: "NOT attr AND NOT resource", + expr: "NOT a = 'a' AND NOT x = 'x'", + wantRSB: "WHERE NOT (x_cond)", + wantSB: "WHERE NOT (a_cond)", + }, + { + // RSB: NOT(a→SkipConditionLiteral)→SkipConditionLiteral → OR short-circuits. + name: "NOT attr OR NOT resource", + expr: "NOT a = 'a' OR NOT x = 'x'", + wantRSB: "WHERE true", + wantSB: "WHERE (NOT (a_cond) OR NOT (x_cond))", + }, + { + // Parses as: (NOT a='a') OR (b='b' AND x='x') + // RSB: NOT(a→SkipConditionLiteral)→SkipConditionLiteral; (SkipConditionLiteral AND x_cond)→x_cond; + // SkipConditionLiteral OR x_cond → SkipConditionLiteral short-circuits OR. + name: "complex NOT OR AND", + expr: "NOT a = 'a' OR b = 'b' AND x = 'x'", + wantRSB: "WHERE true", + wantSB: "WHERE (NOT (a_cond) OR (b_cond AND x_cond))", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := PrepareWhereClause(tt.expr, rsbOpts) + assert.Equal(t, tt.wantErrRSB, err != nil, "resourceConditionBuilder: error expectation mismatch") + if err == nil { + expr, _ := result.WhereClause.Build() + assert.Equal(t, tt.wantRSB, expr, "resourceConditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantRSB, expr) + } + result, err = PrepareWhereClause(tt.expr, sbOpts) + assert.Equal(t, tt.wantErrSB, err != nil, "conditionBuilder: error expectation mismatch") + if err == nil { + expr, _ := result.WhereClause.Build() + assert.Equal(t, tt.wantSB, expr, "conditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantSB, expr) + } + }) + } +} + +// TestVisitComparison_Parens covers parenthesized sub-expressions. +// VisitPrimary adds one extra layer of parens around real conditions; +// TrueConditionLiteral passes through unwrapped. +func TestVisitComparison_Parens(t *testing.T) { + rsbOpts, sbOpts := visitComparisonOpts() + tests := []visitComparisonCase{ + { + // RSB: SkipConditionLiteral passes through unwrapped. SB: VisitPrimary wraps in parens. + name: "single attribute key in parens", + expr: "(a = 'a')", + wantRSB: "WHERE true", + wantSB: "WHERE (a_cond)", + }, + { + // RSB: a→true stripped; VisitPrimary wraps x_cond → "(x_cond)". + name: "AND in parens with mixed keys", + expr: "(a = 'a' AND x = 'x')", + wantRSB: "WHERE (x_cond)", + wantSB: "WHERE (a_cond)", + }, + { + name: "NOT of paren-AND with mixed keys", + expr: "NOT (a = 'a' AND x = 'x')", + wantRSB: "WHERE NOT ((x_cond))", + wantSB: "WHERE NOT ((a_cond))", + }, + { + // RSB: left (a OR b)→true stripped by AND; right (x OR y) survives. + name: "two paren-OR groups ANDed", + expr: "(a = 'a' OR b = 'b') AND (x = 'x' OR y = 'y')", + wantRSB: "WHERE ((x_cond OR y_cond))", + wantSB: "WHERE (((a_cond OR b_cond)) AND ((x_cond OR y_cond)))", + }, + { + // RSB: left (a OR b)→true → OR short-circuits. + name: "two paren-OR groups ORed", + expr: "(a = 'a' OR b = 'b') OR (x = 'x' OR y = 'y')", + wantRSB: "WHERE true", + wantSB: "WHERE (((a_cond OR b_cond)) OR ((x_cond OR y_cond)))", + }, + { + // RSB: a→true stripped; (b OR x) b→true→short-circuits→true stripped; y_cond survives. + name: "paren-OR in middle of three-way AND", + expr: "a = 'a' AND (b = 'b' OR x = 'x') AND y = 'y'", + wantRSB: "WHERE y_cond", + wantSB: "WHERE (a_cond AND ((b_cond OR x_cond)) AND y_cond)", + }, + { + // Each VisitPrimary(paren) adds one extra "()" layer. + name: "deeply nested parentheses", + expr: "(((x = 'x')))", + wantRSB: "WHERE (((x_cond)))", + wantSB: "WHERE true", + }, + { + // RSB: inner NOT(a→SkipConditionLiteral)→SkipConditionLiteral; paren passes through; + // outer NOT(SkipConditionLiteral)→SkipConditionLiteral. + // SB: structural parens accumulate around each NOT. + name: "double NOT via parens", + expr: "NOT (NOT a = 'a')", + wantRSB: "WHERE true", + wantSB: "WHERE NOT ((NOT (a_cond)))", + }, + { + // RSB: all attrs → SkipConditionLiteral filtered from AND → AND returns SkipConditionLiteral; + // paren passes through; NOT(SkipConditionLiteral) → SkipConditionLiteral. + name: "NOT of parenthesized all-attribute AND", + expr: "NOT (a = 'a' AND b = 'b')", + wantRSB: "WHERE true", + wantSB: "WHERE NOT (((a_cond AND b_cond)))", + }, + { + // RSB: a→SkipConditionLiteral short-circuits OR; paren passes through; NOT(SkipConditionLiteral)→SkipConditionLiteral. + name: "NOT of parenthesized mixed OR attr short-circuits", + expr: "NOT (a = 'a' OR x = 'x')", + wantRSB: "WHERE true", + wantSB: "WHERE NOT (((a_cond OR x_cond)))", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := PrepareWhereClause(tt.expr, rsbOpts) + assert.Equal(t, tt.wantErrRSB, err != nil, "resourceConditionBuilder: error expectation mismatch") + if err == nil { + expr, _ := result.WhereClause.Build() + assert.Equal(t, tt.wantRSB, expr, "resourceConditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantRSB, expr) + } + result, err = PrepareWhereClause(tt.expr, sbOpts) + assert.Equal(t, tt.wantErrSB, err != nil, "conditionBuilder: error expectation mismatch") + if err == nil { + expr, _ := result.WhereClause.Build() + assert.Equal(t, tt.wantSB, expr, "conditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantSB, expr) + } + }) + } +} + +// TestVisitComparison_FullText covers full-text (bare string literal) expressions. +// rsbOpts has SkipFullTextFilter=true → TrueConditionLiteral. +// sbOpts has SkipFullTextFilter=false, FullTextColumn=bodyCol → "body_cond". +func TestVisitComparison_FullText(t *testing.T) { + rsbOpts, sbOpts := visitComparisonOpts() + tests := []visitComparisonCase{ + { + name: "standalone full-text term", + expr: "'hello'", + wantRSB: "WHERE true", + wantSB: "WHERE body_cond", + }, + { + // RSB: FT→true, a→true; AND propagates true. + name: "full-text AND attribute", + expr: "'hello' AND a = 'a'", + wantRSB: "WHERE true", + wantSB: "WHERE (body_cond AND a_cond)", + }, + { + // RSB: FT→true stripped; x_cond survives. + name: "full-text AND resource", + expr: "'hello' AND x = 'x'", + wantRSB: "WHERE x_cond", + wantSB: "WHERE body_cond", + }, + { + // RSB: NOT(FT→SkipConditionLiteral)→SkipConditionLiteral. SB: structural NOT applied. + name: "NOT full-text term", + expr: "NOT 'hello'", + wantRSB: "WHERE true", + wantSB: "WHERE NOT (body_cond)", + }, + { + // RSB: FT→true short-circuits OR. + name: "full-text OR resource", + expr: "'hello' OR x = 'x'", + wantRSB: "WHERE true", + wantSB: "WHERE (body_cond OR x_cond)", + }, + { + name: "full-text OR attribute", + expr: "'hello' OR a = 'a'", + wantRSB: "WHERE true", + wantSB: "WHERE (body_cond OR a_cond)", + }, + { + name: "two full-text terms ANDed", + expr: "'hello' AND 'world'", + wantRSB: "WHERE true", + wantSB: "WHERE (body_cond AND body_cond)", + }, + { + name: "two full-text terms ORed", + expr: "'hello' OR 'world'", + wantRSB: "WHERE true", + wantSB: "WHERE (body_cond OR body_cond)", + }, + { + name: "full-text in parentheses", + expr: "('hello')", + wantRSB: "WHERE true", + wantSB: "WHERE (body_cond)", + }, + { + name: "two full-text AND attribute", + expr: "'hello' AND 'world' AND a = 'a'", + wantRSB: "WHERE true", + wantSB: "WHERE (body_cond AND body_cond AND a_cond)", + }, + { + name: "full-text OR attr OR resource all types", + expr: "'hello' OR a = 'a' OR x = 'x'", + wantRSB: "WHERE true", + wantSB: "WHERE (body_cond OR a_cond OR x_cond)", + }, + { + name: "NOT of paren full-text AND attr", + expr: "NOT ('hello' AND a = 'a')", + wantRSB: "WHERE true", + wantSB: "WHERE NOT (((body_cond AND a_cond)))", + }, + { + // RSB: NOT(FT→SkipConditionLiteral)→SkipConditionLiteral stripped from AND; x_cond survives. + name: "NOT full-text AND resource", + expr: "NOT 'hello' AND x = 'x'", + wantRSB: "WHERE x_cond", + wantSB: "WHERE NOT (body_cond)", + }, + { + name: "NOT full-text OR resource", + expr: "NOT 'hello' OR x = 'x'", + wantRSB: "WHERE true", + wantSB: "WHERE (NOT (body_cond) OR x_cond)", + }, + { + // RSB: FT→true stripped; x_cond survives. + name: "full-text AND BETWEEN", + expr: "'hello' AND x BETWEEN 1 AND 3", + wantRSB: "WHERE x_cond", + wantSB: "WHERE body_cond", + }, + { + name: "full-text AND EXISTS", + expr: "'hello' AND x EXISTS", + wantRSB: "WHERE x_cond", + wantSB: "WHERE body_cond", + }, + { + // RSB: FT→true and allVariable→true; AND propagates true. + // SB: allVariable→TrueConditionLiteral stripped; body_cond survives. + name: "full-text AND allVariable", + expr: "'hello' AND x IN $service", + wantRSB: "WHERE true", + wantSB: "WHERE body_cond", + }, + { + // SB: body_cond added first; then allVariable→TrueConditionLiteral short-circuits OR. + name: "full-text OR allVariable", + expr: "'hello' OR x IN $service", + wantRSB: "WHERE true", + wantSB: "WHERE true", + }, + { + // SB: body_cond + name: "full-text with sentinel value", + expr: SkipConditionLiteral, + wantRSB: "WHERE true", + wantSB: "WHERE body_cond", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := PrepareWhereClause(tt.expr, rsbOpts) + assert.Equal(t, tt.wantErrRSB, err != nil, "resourceConditionBuilder: error expectation mismatch") + if err == nil { + expr, _ := result.WhereClause.Build() + assert.Equal(t, tt.wantRSB, expr, "resourceConditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantRSB, expr) + } + result, err = PrepareWhereClause(tt.expr, sbOpts) + assert.Equal(t, tt.wantErrSB, err != nil, "conditionBuilder: error expectation mismatch") + if err == nil { + expr, _ := result.WhereClause.Build() + assert.Equal(t, tt.wantSB, expr, "conditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantSB, expr) + } + }) + } +} + +// TestVisitComparison_AllVariable covers the __all__ dynamic variable. +// IN/NOT IN $service where service="__all__" resolves to TrueConditionLiteral in both +// RSB and SB, short-circuiting OR and being stripped from AND. +// Equality with __all__ does NOT short-circuit — the variable resolves to the literal +// "__all__" string and ConditionFor is called normally. +func TestVisitComparison_AllVariable(t *testing.T) { + rsbOpts, sbOpts := visitComparisonOpts() + tests := []visitComparisonCase{ + { + name: "IN allVariable alone", + expr: "x IN $service", + wantRSB: "WHERE true", + wantSB: "WHERE true", + }, + { + // TrueConditionLiteral stripped from AND; y_cond remains. + name: "IN allVariable AND resource", + expr: "x IN $service AND y = 'y'", + wantRSB: "WHERE y_cond", + wantSB: "WHERE true", + }, + { + // TrueConditionLiteral short-circuits OR. + name: "IN allVariable OR resource", + expr: "x IN $service OR y = 'y'", + wantRSB: "WHERE true", + wantSB: "WHERE true", + }, + { + // RSB: a IN __all__→true stripped; x_cond remains. + // SB (no OR): a IN __all__→true; x filtered → ""; AND: no real conds → true. + name: "attr IN allVariable AND resource", + expr: "a IN $service AND x = 'x'", + wantRSB: "WHERE x_cond", + wantSB: "WHERE true", + }, + { + // NOT IN also resolves __all__ to TrueConditionLiteral. + name: "NOT IN allVariable alone", + expr: "x NOT IN $service", + wantRSB: "WHERE true", + wantSB: "WHERE true", + }, + { + name: "NOT IN allVariable AND resource", + expr: "x NOT IN $service AND y = 'y'", + wantRSB: "WHERE y_cond", + wantSB: "WHERE true", + }, + { + // NOT (x IN $service): __all__ → SkipConditionLiteral; VisitPrimary passes through; + // NOT(SkipConditionLiteral) → SkipConditionLiteral. + name: "NOT of allVariable IN", + expr: "NOT (x IN $service)", + wantRSB: "WHERE true", + wantSB: "WHERE true", + }, + { + name: "allVariable IN AND allVariable IN", + expr: "x IN $service AND y IN $service", + wantRSB: "WHERE true", + wantSB: "WHERE true", + }, + { + // SB: allVariable→TrueConditionLiteral stripped; body_cond survives. + name: "allVariable IN AND full-text", + expr: "x IN $service AND 'hello'", + wantRSB: "WHERE true", + wantSB: "WHERE body_cond", + }, + { + // Equality does not trigger __all__ short-circuit; ConditionFor called normally. + name: "equality with __all__ variable no shortcircuit", + expr: "a = $service", + wantRSB: "WHERE true", + wantSB: "WHERE a_cond", + }, + { + // __all__→TrueConditionLiteral stripped in AND; y_cond wrapped in paren; NOT wraps. + name: "NOT of paren with __all__ AND resource", + expr: "NOT (x IN $service AND y = 'y')", + wantRSB: "WHERE NOT ((y_cond))", + wantSB: "WHERE true", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := PrepareWhereClause(tt.expr, rsbOpts) + assert.Equal(t, tt.wantErrRSB, err != nil, "resourceConditionBuilder: error expectation mismatch") + if err == nil { + expr, _ := result.WhereClause.Build() + assert.Equal(t, tt.wantRSB, expr, "resourceConditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantRSB, expr) + } + result, err = PrepareWhereClause(tt.expr, sbOpts) + assert.Equal(t, tt.wantErrSB, err != nil, "conditionBuilder: error expectation mismatch") + if err == nil { + expr, _ := result.WhereClause.Build() + assert.Equal(t, tt.wantSB, expr, "conditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantSB, expr) + } + }) + } +} + +// TestVisitComparison_FunctionCalls covers function call expressions (has, hasAny, hasAll). +// rsbOpts has SkipFunctionCalls=true → TrueConditionLiteral (function never evaluated). +// sbOpts has SkipFunctionCalls=false; has/hasAny/hasAll only support FieldContextBody, +// so calls on attribute/resource keys return an error. +func TestVisitComparison_FunctionCalls(t *testing.T) { + rsbOpts, sbOpts := visitComparisonOpts() + tests := []visitComparisonCase{ + { + name: "has on attribute key", + expr: "has(a, 'hello')", + wantRSB: "WHERE true", + wantErrSB: true, + }, + { + name: "has on resource key", + expr: "has(x, 'hello')", + wantRSB: "WHERE true", + wantErrSB: true, + }, + { + // RSB: TrueConditionLiteral stripped by AND; x_cond remains. + name: "has AND resource key", + expr: "has(a, 'hello') AND x = 'x'", + wantRSB: "WHERE x_cond", + wantErrSB: true, + }, + { + // RSB: TrueConditionLiteral short-circuits OR. + name: "has OR resource key", + expr: "has(a, 'hello') OR x = 'x'", + wantRSB: "WHERE true", + wantErrSB: true, + }, + { + name: "NOT of has", + expr: "NOT has(a, 'hello')", + wantRSB: "WHERE true", + wantErrSB: true, + }, + { + // RSB: NOT(has→SkipConditionLiteral)→SkipConditionLiteral stripped from AND; y_cond remains. + name: "NOT of has AND resource key", + expr: "NOT has(a, 'hello') AND y = 'y'", + wantRSB: "WHERE y_cond", + wantErrSB: true, + }, + { + // AND binds tighter: (has AND x) OR y. + // RSB: (true AND x_cond)→x_cond; OR y_cond → (x_cond OR y_cond). + name: "has AND resource OR resource", + expr: "has(a, 'hello') AND x = 'x' OR y = 'y'", + wantRSB: "WHERE (x_cond OR y_cond)", + wantErrSB: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := PrepareWhereClause(tt.expr, rsbOpts) + assert.Equal(t, tt.wantErrRSB, err != nil, "resourceConditionBuilder: error expectation mismatch") + if err == nil { + expr, _ := result.WhereClause.Build() + assert.Equal(t, tt.wantRSB, expr, "resourceConditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantRSB, expr) + } + result, err = PrepareWhereClause(tt.expr, sbOpts) + assert.Equal(t, tt.wantErrSB, err != nil, "conditionBuilder: error expectation mismatch") + if err == nil { + expr, _ := result.WhereClause.Build() + assert.Equal(t, tt.wantSB, expr, "conditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantSB, expr) + } + }) + } +} + +// TestVisitComparison_UnknownKeys covers unknown key handling. +// rsbOpts has IgnoreNotFoundKeys=true → VisitComparison returns SkipConditionLiteral +// (no keys resolved); SkipConditionLiteral short-circuits OR and is stripped from AND. +// sbOpts has IgnoreNotFoundKeys=false → key lookup appends an error. +func TestVisitComparison_UnknownKeys(t *testing.T) { + rsbOpts, sbOpts := visitComparisonOpts() + tests := []visitComparisonCase{ + { + // RSB: unknown_key → SkipConditionLiteral (no keys resolved); stripped from AND; x_cond survives. + name: "unknown key AND resource", + expr: "unknown_key = 'val' AND x = 'x'", + wantRSB: "WHERE x_cond", + wantErrSB: true, + }, + { + // RSB: unknown_key not found (IgnoreNotFoundKeys=true) → SkipConditionLiteral; + // SkipConditionLiteral short-circuits OR → x_cond never evaluated → WHERE true. + name: "unknown key OR resource", + expr: "unknown_key = 'val' OR x = 'x'", + wantRSB: "WHERE true", + wantErrSB: true, + }, + { + // RSB: unknown_key → SkipConditionLiteral short-circuits OR → WHERE true (a=a never evaluated). + name: "unknown key OR attribute", + expr: "unknown_key = 'val' OR a = 'a'", + wantRSB: "WHERE true", + wantErrSB: true, + }, + { + // RSB: both → SkipConditionLiteral; all stripped from AND → AND returns SkipConditionLiteral → WHERE true. + name: "all unknown keys in AND", + expr: "unk1 = 'v' AND unk2 = 'v'", + wantRSB: "WHERE true", + wantErrSB: true, + }, + { + // RSB: unknown_key → SkipConditionLiteral; NOT(SkipConditionLiteral) → SkipConditionLiteral (guard); + // PrepareWhereClause converts to WHERE true. + name: "NOT of unknown key", + expr: "NOT unknown_key = 'val'", + wantRSB: "WHERE true", + wantErrSB: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := PrepareWhereClause(tt.expr, rsbOpts) + assert.Equal(t, tt.wantErrRSB, err != nil, "resourceConditionBuilder: error expectation mismatch") + if err == nil { + expr, _ := result.WhereClause.Build() + assert.Equal(t, tt.wantRSB, expr, "resourceConditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantRSB, expr) + } + result, err = PrepareWhereClause(tt.expr, sbOpts) + assert.Equal(t, tt.wantErrSB, err != nil, "conditionBuilder: error expectation mismatch") + if err == nil { + expr, _ := result.WhereClause.Build() + assert.Equal(t, tt.wantSB, expr, "conditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantSB, expr) + } + }) + } +} + +// TestVisitComparison_SkippableLiteralValues guards against two distinct collision risks +// involving SkippableConditionLiterals ("true", "__skip__", "__skip_because_of_error__"): +func TestVisitComparison_SkippableLiteralValues(t *testing.T) { + rsbOpts, sbOpts := visitComparisonOpts() + + tests := []visitComparisonCase{ + { + // rsbOpts: a is attr → SkipConditionLiteral → WHERE true. + // sbOpts: conditionBuilder ignores value → WHERE a_cond. + name: "value equals TrueConditionLiteral", + expr: fmt.Sprintf("a = '%s'", TrueConditionLiteral), + wantRSB: "WHERE true", + wantSB: "WHERE a_cond", + }, + { + name: "value equals SkipConditionLiteral", + expr: fmt.Sprintf("a = '%s'", SkipConditionLiteral), + wantRSB: "WHERE true", + wantSB: "WHERE a_cond", + }, + { + name: "value equals ErrorConditionLiteral", + expr: fmt.Sprintf("a = '%s'", ErrorConditionLiteral), + wantRSB: "WHERE true", + wantSB: "WHERE a_cond", + }, + { + // IN list whose members are all sentinel literals. + name: "IN list containing all sentinel literals", + expr: fmt.Sprintf("a IN ('%s', '%s', '%s')", TrueConditionLiteral, SkipConditionLiteral, ErrorConditionLiteral), + wantRSB: "WHERE true", + wantSB: "WHERE a_cond", + }, + { + // Both a and b are attribute keys → rsbOpts → WHERE true. + // sbOpts → two real conditions joined by AND. + name: "AND with sentinel value on one branch", + expr: fmt.Sprintf("a = '%s' AND b = 'real_value'", SkipConditionLiteral), + wantRSB: "WHERE true", + wantSB: "WHERE (a_cond AND b_cond)", + }, + { + // rsbOpts: NOT(SkipConditionLiteral) → SkipConditionLiteral → WHERE true. + // sbOpts: NOT wraps the real condition. + name: "NOT with sentinel value", + expr: fmt.Sprintf("NOT a = '%s'", TrueConditionLiteral), + wantRSB: "WHERE true", + wantSB: "WHERE NOT (a_cond)", + }, + { + // SkipConditionLiteral as a bare full-text search term. + // rsbOpts: SkipFullTextFilter=true → SkipConditionLiteral → WHERE true. + // sbOpts: full-text search on body column → WHERE body_cond. + name: "full text search with SkipConditionLiteral", + expr: SkipConditionLiteral, + wantRSB: "WHERE true", + wantSB: "WHERE body_cond", + }, + { + // TrueConditionLiteral as a bare full-text search term. + // rsbOpts: SkipFullTextFilter=true → SkipConditionLiteral → WHERE true. + // sbOpts: full-text search on body column → WHERE body_cond. + name: "full text search with TrueConditionLiteral", + expr: TrueConditionLiteral, + wantRSB: "WHERE true", + wantSB: "WHERE body_cond", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := PrepareWhereClause(tt.expr, rsbOpts) + assert.Equal(t, tt.wantErrRSB, err != nil, "resourceConditionBuilder: error expectation mismatch") + if err == nil { + expr, _ := result.WhereClause.Build() + assert.Equal(t, tt.wantRSB, expr, "resourceConditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantRSB, expr) + } + result, err = PrepareWhereClause(tt.expr, sbOpts) + assert.Equal(t, tt.wantErrSB, err != nil, "conditionBuilder: error expectation mismatch") + if err == nil { + expr, _ := result.WhereClause.Build() + assert.Equal(t, tt.wantSB, expr, "conditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantSB, expr) + } + }) + } +} diff --git a/pkg/telemetrylogs/stmt_builder_test.go b/pkg/telemetrylogs/stmt_builder_test.go index 31223bdae45..054faebc8ec 100644 --- a/pkg/telemetrylogs/stmt_builder_test.go +++ b/pkg/telemetrylogs/stmt_builder_test.go @@ -109,8 +109,8 @@ func TestStatementBuilderTimeSeries(t *testing.T) { }, }, expected: qbtypes.Statement{ - Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) OR true) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, countDistinct(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL) OR (attributes_string['http.method'] = ? AND mapContains(attributes_string, 'http.method') = ?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, countDistinct(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL) OR (attributes_string['http.method'] = ? AND mapContains(attributes_string, 'http.method') = ?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`", - Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1705224600), uint64(1705485600), "redis-manual", "GET", true, "1705226400000000000", uint64(1705224600), "1705485600000000000", uint64(1705485600), 10, "redis-manual", "GET", true, "1705226400000000000", uint64(1705224600), "1705485600000000000", uint64(1705485600)}, + Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, countDistinct(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL) OR (attributes_string['http.method'] = ? AND mapContains(attributes_string, 'http.method') = ?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, countDistinct(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL) OR (attributes_string['http.method'] = ? AND mapContains(attributes_string, 'http.method') = ?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`", + Args: []any{uint64(1705224600), uint64(1705485600), "redis-manual", "GET", true, "1705226400000000000", uint64(1705224600), "1705485600000000000", uint64(1705485600), 10, "redis-manual", "GET", true, "1705226400000000000", uint64(1705224600), "1705485600000000000", uint64(1705485600)}, }, expectedErr: nil, }, @@ -206,7 +206,7 @@ func TestStatementBuilderTimeSeries(t *testing.T) { Limit: 10, }, expected: qbtypes.Statement{ - Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (true OR true) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 SECOND) AS ts, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((match(`attribute_string_materialized$$key$$name`, ?) AND `attribute_string_materialized$$key$$name_exists` = ?) OR (`attribute_string_materialized$$key$$name` = ? AND `attribute_string_materialized$$key$$name_exists` = ?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY ts", + Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 SECOND) AS ts, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((match(`attribute_string_materialized$$key$$name`, ?) AND `attribute_string_materialized$$key$$name_exists` = ?) OR (`attribute_string_materialized$$key$$name` = ? AND `attribute_string_materialized$$key$$name_exists` = ?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY ts", Args: []any{uint64(1705397400), uint64(1705485600), "redis.*", true, "memcached", true, "1705399200000000000", uint64(1705397400), "1705485600000000000", uint64(1705485600)}, }, expectedErr: nil, @@ -331,7 +331,7 @@ func TestStatementBuilderListQuery(t *testing.T) { }, }, expected: qbtypes.Statement{ - Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (true OR true) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((match(`attribute_string_materialized$$key$$name`, ?) AND `attribute_string_materialized$$key$$name_exists` = ?) OR (`attribute_string_materialized$$key$$name` = ? AND `attribute_string_materialized$$key$$name_exists` = ?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? ORDER BY `attribute_string_materialized$$key$$name` AS `materialized.key.name` desc LIMIT ?", + Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((match(`attribute_string_materialized$$key$$name`, ?) AND `attribute_string_materialized$$key$$name_exists` = ?) OR (`attribute_string_materialized$$key$$name` = ? AND `attribute_string_materialized$$key$$name_exists` = ?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? ORDER BY `attribute_string_materialized$$key$$name` AS `materialized.key.name` desc LIMIT ?", Args: []any{uint64(1747945619), uint64(1747983448), "redis.*", true, "memcached", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10}, }, expectedErr: nil, @@ -427,7 +427,7 @@ func TestStatementBuilderListQueryResourceTests(t *testing.T) { }, }, expected: qbtypes.Statement{ - Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (match(LOWER(body), LOWER(?))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? ORDER BY `attribute_string_materialized$$key$$name` AS `materialized.key.name` desc LIMIT ?", + Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND match(LOWER(body), LOWER(?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? ORDER BY `attribute_string_materialized$$key$$name` AS `materialized.key.name` desc LIMIT ?", Args: []any{"cartservice", "%service.name%", "%service.name\":\"cartservice%", uint64(1747945619), uint64(1747983448), "hello", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10}, }, expectedErr: nil, @@ -620,7 +620,7 @@ func TestStatementBuilderListQueryServiceCollision(t *testing.T) { Limit: 10, }, expected: qbtypes.Statement{ - Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND true)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((LOWER(body) LIKE LOWER(?))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", + Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (LOWER(body) LIKE LOWER(?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?", Args: []any{"cartservice", "%service.name%", "%service.name\":\"cartservice%", uint64(1747945619), uint64(1747983448), "%error%", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10}, }, expectedErr: nil, @@ -649,7 +649,7 @@ func TestStatementBuilderListQueryServiceCollision(t *testing.T) { }, }, expected: qbtypes.Statement{ - Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND true) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (LOWER(body) LIKE LOWER(?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? ORDER BY `attribute_string_materialized$$key$$name` AS `materialized.key.name` desc LIMIT ?", + Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND LOWER(body) LIKE LOWER(?) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? ORDER BY `attribute_string_materialized$$key$$name` AS `materialized.key.name` desc LIMIT ?", Args: []any{"cartservice", "%service.name%", "%service.name\":\"cartservice%", uint64(1747945619), uint64(1747983448), "%error%", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10}, }, expectedErr: nil, diff --git a/pkg/telemetrytraces/stmt_builder_test.go b/pkg/telemetrytraces/stmt_builder_test.go index 026e83062b7..75598ca45e7 100644 --- a/pkg/telemetrytraces/stmt_builder_test.go +++ b/pkg/telemetrytraces/stmt_builder_test.go @@ -90,8 +90,8 @@ func TestStatementBuilder(t *testing.T) { }, }, expected: qbtypes.Statement{ - Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) OR true) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL) OR (attributes_string['http.request.method'] = ? AND mapContains(attributes_string, 'http.request.method') = ?)) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL) OR (attributes_string['http.request.method'] = ? AND mapContains(attributes_string, 'http.request.method') = ?)) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`", - Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "redis-manual", "GET", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, "redis-manual", "GET", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)}, + Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL) OR (attributes_string['http.request.method'] = ? AND mapContains(attributes_string, 'http.request.method') = ?)) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL) OR (attributes_string['http.request.method'] = ? AND mapContains(attributes_string, 'http.request.method') = ?)) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`", + Args: []any{uint64(1747945619), uint64(1747983448), "redis-manual", "GET", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, "redis-manual", "GET", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)}, }, expectedErr: nil, }, @@ -119,8 +119,8 @@ func TestStatementBuilder(t *testing.T) { }, }, expected: qbtypes.Statement{ - Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (true OR (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((match(`attribute_string_materialized$$key$$name`, ?) AND `attribute_string_materialized$$key$$name_exists` = ?) OR (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL)) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((match(`attribute_string_materialized$$key$$name`, ?) AND `attribute_string_materialized$$key$$name_exists` = ?) OR (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL)) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`", - Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "redis-manual", true, "redis-manual", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, "redis-manual", true, "redis-manual", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)}, + Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((match(`attribute_string_materialized$$key$$name`, ?) AND `attribute_string_materialized$$key$$name_exists` = ?) OR (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL)) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL), NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((match(`attribute_string_materialized$$key$$name`, ?) AND `attribute_string_materialized$$key$$name_exists` = ?) OR (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL)) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`", + Args: []any{uint64(1747945619), uint64(1747983448), "redis-manual", true, "redis-manual", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, "redis-manual", true, "redis-manual", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)}, }, expectedErr: nil, }, @@ -856,8 +856,8 @@ func TestStatementBuilderTraceQuery(t *testing.T) { Limit: 10, }, expected: qbtypes.Statement{ - Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (true OR (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __toe AS (SELECT trace_id FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((match(`attribute_string_materialized$$key$$name`, ?) AND `attribute_string_materialized$$key$$name_exists` = ?) OR (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL)) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __toe_duration_sorted AS (SELECT trace_id, duration_nano, resource_string_service$$name as `service.name`, name FROM signoz_traces.distributed_signoz_index_v3 WHERE parent_span_id = '' AND trace_id GLOBAL IN __toe AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY duration_nano DESC LIMIT 1 BY trace_id) SELECT __toe_duration_sorted.`service.name` AS `service.name`, __toe_duration_sorted.name AS `name`, count() AS span_count, __toe_duration_sorted.duration_nano AS `duration_nano`, __toe_duration_sorted.trace_id AS `trace_id` FROM __toe INNER JOIN __toe_duration_sorted ON __toe.trace_id = __toe_duration_sorted.trace_id GROUP BY trace_id, duration_nano, name, `service.name` ORDER BY duration_nano DESC LIMIT 1 BY trace_id LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000", - Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "redis-manual", true, "redis-manual", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10}, + Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __toe AS (SELECT trace_id FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((match(`attribute_string_materialized$$key$$name`, ?) AND `attribute_string_materialized$$key$$name_exists` = ?) OR (multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) = ? AND multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) IS NOT NULL)) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __toe_duration_sorted AS (SELECT trace_id, duration_nano, resource_string_service$$name as `service.name`, name FROM signoz_traces.distributed_signoz_index_v3 WHERE parent_span_id = '' AND trace_id GLOBAL IN __toe AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY duration_nano DESC LIMIT 1 BY trace_id) SELECT __toe_duration_sorted.`service.name` AS `service.name`, __toe_duration_sorted.name AS `name`, count() AS span_count, __toe_duration_sorted.duration_nano AS `duration_nano`, __toe_duration_sorted.trace_id AS `trace_id` FROM __toe INNER JOIN __toe_duration_sorted ON __toe.trace_id = __toe_duration_sorted.trace_id GROUP BY trace_id, duration_nano, name, `service.name` ORDER BY duration_nano DESC LIMIT 1 BY trace_id LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000", + Args: []any{uint64(1747945619), uint64(1747983448), "redis-manual", true, "redis-manual", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10}, }, expectedErr: nil, }, @@ -902,7 +902,7 @@ func TestStatementBuilderTraceQuery(t *testing.T) { Limit: 10, }, expected: qbtypes.Statement{ - Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (true OR true) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __toe AS (SELECT trace_id FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (((name, resource_string_service$$name) GLOBAL IN (SELECT DISTINCT name, serviceName from signoz_traces.distributed_top_level_operations WHERE time >= toDateTime(1747947419))) AND parent_span_id != '' OR (`attribute_string_materialized$$key$$name` = ? AND `attribute_string_materialized$$key$$name_exists` = ?)) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __toe_duration_sorted AS (SELECT trace_id, duration_nano, resource_string_service$$name as `service.name`, name FROM signoz_traces.distributed_signoz_index_v3 WHERE parent_span_id = '' AND trace_id GLOBAL IN __toe AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY duration_nano DESC LIMIT 1 BY trace_id) SELECT __toe_duration_sorted.`service.name` AS `service.name`, __toe_duration_sorted.name AS `name`, count() AS span_count, __toe_duration_sorted.duration_nano AS `duration_nano`, __toe_duration_sorted.trace_id AS `trace_id` FROM __toe INNER JOIN __toe_duration_sorted ON __toe.trace_id = __toe_duration_sorted.trace_id GROUP BY trace_id, duration_nano, name, `service.name` ORDER BY duration_nano DESC LIMIT 1 BY trace_id LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000", + Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __toe AS (SELECT trace_id FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (((name, resource_string_service$$name) GLOBAL IN (SELECT DISTINCT name, serviceName from signoz_traces.distributed_top_level_operations WHERE time >= toDateTime(1747947419))) AND parent_span_id != '' OR (`attribute_string_materialized$$key$$name` = ? AND `attribute_string_materialized$$key$$name_exists` = ?)) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __toe_duration_sorted AS (SELECT trace_id, duration_nano, resource_string_service$$name as `service.name`, name FROM signoz_traces.distributed_signoz_index_v3 WHERE parent_span_id = '' AND trace_id GLOBAL IN __toe AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY duration_nano DESC LIMIT 1 BY trace_id) SELECT __toe_duration_sorted.`service.name` AS `service.name`, __toe_duration_sorted.name AS `name`, count() AS span_count, __toe_duration_sorted.duration_nano AS `duration_nano`, __toe_duration_sorted.trace_id AS `trace_id` FROM __toe INNER JOIN __toe_duration_sorted ON __toe.trace_id = __toe_duration_sorted.trace_id GROUP BY trace_id, duration_nano, name, `service.name` ORDER BY duration_nano DESC LIMIT 1 BY trace_id LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000", Args: []any{uint64(1747945619), uint64(1747983448), "redis-manual", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10}, }, expectedErr: nil, diff --git a/tests/integration/fixtures/querier.py b/tests/integration/fixtures/querier.py index d42c628a027..9d24579dfdf 100644 --- a/tests/integration/fixtures/querier.py +++ b/tests/integration/fixtures/querier.py @@ -519,6 +519,16 @@ def get_scalar_columns(response_json: Dict) -> List[Dict]: return results[0].get("columns", []) +def get_column_data_from_response(response_json: Dict, column_name: str) -> List[Any]: + results = response_json.get("data", {}).get("data", {}).get("results", []) + if not results: + return [] + rows = results[0].get("rows") or [] + return [ + row["data"][column_name] for row in rows if column_name in row.get("data", {}) + ] + + def assert_scalar_result_order( data: List[List[Any]], expected_order: List[tuple], diff --git a/tests/integration/src/querier/08_filter_expression.py b/tests/integration/src/querier/08_filter_expression.py new file mode 100644 index 00000000000..266b164d5ed --- /dev/null +++ b/tests/integration/src/querier/08_filter_expression.py @@ -0,0 +1,289 @@ +import os +from concurrent.futures import ThreadPoolExecutor, as_completed +from datetime import datetime, timedelta, timezone +from http import HTTPStatus +from typing import Callable, List, Set + +import pytest +import requests + +from fixtures import types +from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD +from fixtures.logs import Logs +from fixtures.querier import get_column_data_from_response, make_query_request + +TESTDATA_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "testdata") +FILTER_EXPRESSIONS_FILE = os.path.join(TESTDATA_DIR, "filter_expressions_10000.txt") + + +@pytest.mark.parametrize( + "expression,expected_logs", + [ + # NOT on resource attribute unique to alpha → beta only + pytest.param( + 'NOT resource.region = "us-east"', + {"beta-log"}, + id="not_resource_attr", + ), + # NOT on number attribute unique to alpha → beta only + pytest.param( + "NOT attribute.status_code = 200", + {"beta-log"}, + id="not_number_attr", + ), + # NOT (OR covering both logs) → no logs + # resource.region = "us-east" matches alpha; resource.status_code = "500" matches beta + pytest.param( + 'NOT (resource.region = "us-east" OR resource.status_code = "500")', + set(), + id="not_or_covering_all", + ), + # Multiple NOTs ANDed: each NOT excludes one log → no logs + pytest.param( + 'NOT resource.region = "us-east" AND NOT resource.status_code = "500"', + set(), + id="multiple_nots_and", + ), + # Double NOT: NOT (NOT expr) ≡ expr → alpha only + pytest.param( + 'NOT (NOT resource.region = "us-east")', + {"alpha-log"}, + id="double_not", + ), + # NOT EXISTS: resource.region exists only on alpha → beta only + pytest.param( + "NOT resource.region EXISTS", + {"beta-log"}, + id="not_exists", + ), + # NOT IN on resource attribute unique to beta → alpha only + pytest.param( + 'resource.status_code NOT IN ["500"]', + {"alpha-log"}, + id="not_in", + ), + # NOT compound (number attr AND resource attr both on alpha) → beta only + pytest.param( + 'NOT (attribute.status_code = 200 AND resource.region = "us-east")', + {"beta-log"}, + id="not_compound_attr_and_resource", + ), + # NOT wrapping impossible AND: no log has both resource.region and resource.status_code + # → AND always false → NOT always true → both logs + pytest.param( + 'NOT (resource.region = "us-east" AND resource.status_code = "500")', + {"alpha-log", "beta-log"}, + id="not_impossible_and", + ), + # IS and NULL are not grammar keywords so they lex as KEY tokens. + # The implicit AND in andExpression splits this into three unary expressions: + # NOT (body REGEXP 'resource.region') AND (body REGEXP 'IS') AND (body REGEXP 'NULL') + # Neither body ("alpha-log", "beta-log") matches "IS" or "NULL" → empty set. + pytest.param( + "NOT resource.region IS NULL", + set(), + id="sql_is_null_as_fulltext", + ), + pytest.param( + "NOT 'NOT'", + {"alpha-log", "beta-log"}, + id="not_full_text_search", + ), + ], +) +def test_not_filter_expression( + signoz: types.SigNoz, + create_user_admin: None, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], + insert_logs: Callable[[List[Logs]], None], + expression: str, + expected_logs: Set[str], +) -> None: + """ + Verifies that valid NOT filter expressions return the correct set of logs. + + Insert the two canonical test logs used by all valid-expression test cases. + + alpha-log: resources region="us-east", env="production", hostname="host-alpha" + attributes status_code=200, latency_ms=350, error_count=3 (numbers) + beta-log: resources status_code="500", latency_ms="2500", error_count="10" + attributes region=1, env=2, hostname=3 (numbers) + + region/env/hostname are present as resource on alpha and as attribute on beta. + status_code/latency_ms/error_count are present as attribute on alpha and as resource on beta. + This intentional overlap exercises context-prefix disambiguation. + """ + now = datetime.now(tz=timezone.utc) + insert_logs( + [ + Logs( + timestamp=now - timedelta(seconds=5), + body="alpha-log", + resources={ + "region": "us-east", + "env": "production", + "hostname": "host-alpha", + }, + attributes={ + "status_code": 200, + "latency_ms": 350, + "error_count": 3, + }, + ), + Logs( + timestamp=now - timedelta(seconds=3), + body="beta-log", + resources={ + "status_code": "500", + "latency_ms": "2500", + "error_count": "10", + }, + attributes={ + "region": 1, + "env": 2, + "hostname": 3, + }, + ), + ] + ) + + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + + now = datetime.now(tz=timezone.utc) + + response = make_query_request( + signoz, + token, + start_ms=int((now - timedelta(seconds=30)).timestamp() * 1000), + end_ms=int(now.timestamp() * 1000), + request_type="raw", + queries=[ + { + "type": "builder_query", + "spec": { + "name": "A", + "signal": "logs", + "disabled": False, + "limit": 100, + "offset": 0, + "filter": {"expression": expression}, + "order": [ + {"key": {"name": "timestamp"}, "direction": "desc"}, + {"key": {"name": "id"}, "direction": "desc"}, + ], + "having": {"expression": ""}, + "aggregations": [{"expression": "count()"}], + }, + } + ], + ) + assert response.status_code == HTTPStatus.OK + assert response.json()["status"] == "success" + assert set(get_column_data_from_response(response.json(), "body")) == expected_logs + + +def test_filter_expressions_no_server_error( + signoz: types.SigNoz, + create_user_admin: None, # pylint: disable=unused-argument + insert_logs, + get_token: Callable[[str, str], str], +) -> None: + """ + Reads every line from filter_expressions_10000.txt and fires it as a filter + expression against the logs query endpoint. + + Expressions may be valid (200) or invalid (400) — both are acceptable. + A 500 means the server crashed on the input and is a test failure. + All failing expressions are collected before asserting so the full list is + visible in one run. + """ + + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + + now = datetime.now(tz=timezone.utc) + insert_logs( + [ + Logs( + timestamp=now - timedelta(seconds=5), + body="alpha-log", + resources={ + "f1": "v10", + "f2": "v20", + "f3": "v30", + }, + attributes={ + "f4": 40, + "f5": 50, + "f6": 60, + }, + ), + Logs( + timestamp=now - timedelta(seconds=3), + body="beta-log", + resources={ + "f4": "v41", + "f5": "v51", + "f6": "v61", + }, + attributes={ + "f1": 11, + "f2": 21, + "f3": 31, + }, + ), + ] + ) + + def _make_raw_logs_query( + signoz: types.SigNoz, + token: str, + filter_expression: str, + ) -> requests.Response: + """Helper to query raw logs with a filter expression over the last 30 seconds.""" + now = datetime.now(tz=timezone.utc) + return make_query_request( + signoz, + token, + start_ms=int((now - timedelta(seconds=30)).timestamp() * 1000), + end_ms=int(now.timestamp() * 1000), + request_type="raw", + queries=[ + { + "type": "builder_query", + "spec": { + "name": "A", + "signal": "logs", + "disabled": False, + "limit": 100, + "offset": 0, + "filter": {"expression": filter_expression}, + "order": [ + {"key": {"name": "timestamp"}, "direction": "desc"}, + {"key": {"name": "id"}, "direction": "desc"}, + ], + "having": {"expression": ""}, + "aggregations": [{"expression": "count()"}], + }, + } + ], + ) + + failures: List[str] = [] + with ThreadPoolExecutor(max_workers=40) as executor: + with open(FILTER_EXPRESSIONS_FILE, encoding="utf-8") as f: + futures = { + executor.submit( + _make_raw_logs_query, signoz, token, expr.rstrip("\n") + ): expr.rstrip("\n") + for expr in f + } + for future in as_completed(futures): + expr = futures[future] + if future.result().status_code == HTTPStatus.INTERNAL_SERVER_ERROR: + failures.append(expr) + + assert ( + len(failures) <= 0 + ), f"{len(failures)} expression(s) caused HTTP 500:\n" + "\n".join( + f" {expr!r}" for expr in failures + ) diff --git a/tests/integration/testdata/filter_expressions_10000.txt b/tests/integration/testdata/filter_expressions_10000.txt new file mode 100644 index 00000000000..99413da8758 --- /dev/null +++ b/tests/integration/testdata/filter_expressions_10000.txt @@ -0,0 +1,10000 @@ +f6 < "abc" +((hasToken(f1, -1)) AND (f2 <> f6)) AND (hasToken(f2, 0.0000000001)) +f1 IN (f1, "%wild%") +f3 BETWEEN -2.7 AND 3.14 +f3 IN ("xyz%", -1, f3, .5, "\n") +f6 IN ('', -1e-10, -10, -1, 0.0000000001) +f1 OR OR '' +f6 IN (-3.5e-2) +f2 BETWEEN f3 AND "\n" +(hasAll(f2, 10)) AND (f3 <= "hello world") +hasAll(f6, 999999999999) +f6 NOT CONTAINS "hello world" +f6 BETWEEN "xyz%" AND f1 +f4 === -.5 +f4 BETWEEN "xyz%" +f2 NOT CONTAINS "abc" +f6 OR OR f6 +f5 BETWEEN AND 0 +has(f1, f5) +f5 IN (,) +(f4 IN (f2, .5, "abc", 0.0000000001, -10)) OR (f5 <> 0.0000000001) +(f2 == -1) OR (f5 <> "hello world") +f5 BETWEEN -.5 AND f4 +f4 NOT ILIKE "xyz%" +f3 CONTAINS "%wild%" +(((f4 > -2.7) AND (f2 BETWEEN f2 AND -.5)) OR (f3 BETWEEN f3 AND "abc")) AND (f5 NOT LIKE "abc") +has(f1, "abc") +f4 NOT ILIKE "%wild%" +AND f2 = 0 +f1 BETWEEN 10 AND "a_b%" +hasAny(f4, 10) +f1 IN () +hasAll(f5, "xyz%") +(f3 BETWEEN "\t" AND -.5) OR (f3 NOT LIKE "abc") +has(f1, 999999999999) +f3 BETWEEN -10 AND "abc" +f4 BETWEEN 'O''Reilly' AND 0 +has(f3, -.5) +f6 != "hello world" +f6 BETWEEN "%wild%" AND 999999999999 +f5 IN ("xyz%", 10, 3.14, f1) +f1 LIKE +(hasAll(f1, 6.02e23)) OR (f1 BETWEEN 'O''Reilly' AND '') +f1 === "abc" +f2 LIKE +f4 <> -1e-10 +f4 NOT +f3 IN [1 2 3] +(f3 = f6 +f2 BETWEEN -1e-10 +((hasToken(f3, "a_b%")) OR ((hasAny(f2, TRUE)) OR ((f2 < "\t") AND (f3 < 999999999999)))) OR (hasAll(f5, f3)) +(hasAny(f4, 3.14)) OR (hasAll(f6, 3.14)) +f2 IN () +(f6 BETWEEN "\n" AND f5) OR (f2 BETWEEN 0 AND -2.7) +NOT +f5 NOT +f6 NOT +hasAny(f1, 6.02e23) +((f4 <= "%wild%") OR (f1 NOT ILIKE "%wild%")) OR (has(f6, "\n")) +f3 IN (TRUE, 0.0000000001, f5, 0.0000000001) +f1 CONTAINS "a_b%" +f6 ILIKE ' ' +f5 BETWEEN AND 1 +f2 IN ("a_b%", "%wild%", "\n", "hello world") +f4 BETWEEN AND "xyz%" +f2 IS NULL +f2 IN (.5, -3.5e-2, 'O''Reilly', -2.7, 1) +f5 IN [1 2 3] +f3 = -2.7) +f6 IN [1 2 3] +f1 BETWEEN "hello world" AND f2 +f5 CONTAINS "a_b%" +f2 IN (999999999999, ' ', "xyz%", "%wild%") +hasAll(f6, 0) +f1 BETWEEN "abc" AND TRUE +f6 BETWEEN f4 AND f5 +f1 < 0.0000000001 +f3 BETWEEN '' AND f4 +f4 IN ("%wild%", ' ', f3, "hello world", 'O''Reilly') +f6 <> "a_b%" +hasAny(f6, -1e-10) +f3 REGEXP "xyz%" +f3 BETWEEN AND 0.0000000001 +((f1 < f4) OR (f3 ILIKE "abc")) OR (f1 NOT LIKE "%wild%") +has(f6, 6.02e23) +has(f3, "abc") +f5 NOT ILIKE '' +f3 CONTAINS 'O''Reilly' +f5 == 999999999999 +f6 IN (-3.5e-2) +hasAny(f1, 0) +(f3 LIKE 'test') OR (((f6 <> "xyz%") AND (f1 NOT ILIKE "\t")) AND (f2 IN (10))) +f5 IN ('test', f2, .5, f3, 'O''Reilly') +f5 IN (f5, f2, 3.14, -1, f1) +has(f3, 3.14) +(hasAll(f1, -1)) AND (f5 NOT CONTAINS 'O''Reilly') +hasToken(f4, 3.14) +f3 NOT REGEXP "\t" +f3 BETWEEN TRUE AND f5 +f5 LIKE ' ' +f3 IN ("\t") +f1 = 'unclosed +f3 BETWEEN f2 AND f5 +(f6 BETWEEN -3.5e-2 AND 'O''Reilly') AND (has(f4, 0.0000000001)) +f1 = NULL +f2 = "hello world") +has(f3, 'test') +hasToken(f5, 6.02e23) +f3 NOT REGEXP '' +f4 BETWEEN "\n" +(f6 ILIKE ' ') AND (f5 BETWEEN "%wild%" AND -2.7) +f4 NOT +((f6 IN ("\n", "a_b%", f5, -1, -10)) AND ((f2 IN (f1)) AND ((f1 >= -3.5e-2) OR (f1 = "a_b%")))) AND (f3 IN (f1, 6.02e23, "abc")) +hasAny(f6, -3.5e-2) +hasAny(f4, f5) +() +f3 REGEXP "\t" +(f6 = -2.7 +hasAll(f3, 1) +has(f6) +hasAny(f3, -2.7) +f6 BETWEEN '' +f2 BETWEEN -.5 AND '' +f2 IN (f4, 'test', "%wild%") +f4 NOT +f3 REGEXP "\n" +(f6 <> f4) AND (hasAll(f5, 0.0000000001)) +(((f2 = -1) OR (f1 NOT LIKE 'test')) AND (f4 NOT REGEXP "\t")) OR (hasAny(f6, TRUE)) +f5 CONTAINS 'O''Reilly' +f2 IN (f2, "abc", '', -2.7) +f3 = 'unclosed +has(f1, "%wild%") +f5 BETWEEN 999999999999 AND -10 +AND f5 = 999999999999 +f1 BETWEEN 1 AND -.5 +f5 BETWEEN "a_b%" AND 1 +f2 <= .5 +f4 NOT REGEXP '' +f5 NOT ILIKE "\n" +(f4 IN (10)) OR (f4 NOT LIKE ' ') +f1 NOT LIKE "a_b%" +f2 < "a_b%" +hasAll(f5, f3) +f1 NOT LIKE "\t" +hasAny(f6, f1) +f4 BETWEEN "%wild%" AND f3 +(f4 IN ("xyz%", '', 6.02e23)) OR (f1 == 3.14) +(f2 IN (10, "%wild%", .5, TRUE, ' ')) OR ((f5 IN (' ', -2.7, f6)) AND (f6 BETWEEN f4 AND 0.0000000001)) +f3 === "hello world" +f3 IN (0, 1e10, 6.02e23) +f1 ILIKE "\t" +f1 BETWEEN "%wild%" +f5 = f2) +f4 REGEXP 'test' +(f1 = -1 +f1 LIKE +(f2 BETWEEN "\n" AND 0.0000000001) OR (f4 = f1) +f3 <= "\t" +f3 NOT LIKE "%wild%" +f2 IN (f3, '', 3.14, f1, -2.7) +has(f4, "\t") +f6 = 'unclosed +AND f3 = 6.02e23 +f6 IS NULL +f6 REGEXP 'O''Reilly' +f5 IN ('O''Reilly') +f1 > 'test' +f5 <= f3 +(f5 NOT ILIKE "a_b%") OR (has(f2, f2)) +f6 IN (f6) +f2 >= f2 +hasAll(f2, '') +f4 IN (,) +(f3 BETWEEN 3.14 AND 6.02e23) OR (f6 IN (-.5, 3.14, 'O''Reilly', "\n")) +f2 IN (3.14, f1, f6, '', 1) +f2 BETWEEN "%wild%" AND f5 +f2 BETWEEN -10 AND 'test' +f1 === "%wild%" +f4 > "abc" +f5 IN [1 2 3] +f4 BETWEEN AND 'test' +f5 IN (.5) +f3 BETWEEN f2 AND f6 +f1 NOT ILIKE ' ' +hasAll(f1) +f6 > 6.02e23 +f4 IN (f4, f2) +f1 < 999999999999 +f5 = "abc") +has(f4, .5) +hasAny(f4) +f6 BETWEEN 3.14 AND '' +has(f5, "\n") +f1 LIKE +f6 === -1 +f1 IN (,) +hasAll(f4, -1) +(f2 = '' +f3 ILIKE ' ' +f5 IN (3.14, f1, 3.14) +f5 BETWEEN 'test' AND "a_b%" +f6 IN ('O''Reilly', f2, f1) +f3 NOT CONTAINS "abc" +f5 IN ("hello world", 10, -10, "\t") +(f1 BETWEEN 6.02e23 AND f2) AND (f1 BETWEEN "abc" AND '') +hasAny(f1, f2) +f3 LIKE +f4 BETWEEN "\n" AND .5 +f6 IN (999999999999, 'test', f5, -1e-10) +f4 != f2 +NOT +f2 BETWEEN "abc" AND f5 +hasAll(f5, f3) +f5 IN [1 2 3] +f6 IN (f2) +hasAny(f3, 1e10) +f2 BETWEEN 6.02e23 +() +(hasAny(f5, -3.5e-2)) OR (has(f1, 3.14)) +(f6 IN (TRUE, 1)) OR ((f6 IN (f4, "abc", "\t")) OR (f1 LIKE "hello world")) +f4 NOT ILIKE "a_b%" +f6 IN (' ', "\t", -.5) +AND f5 = "a_b%" +f4 IN (TRUE, "abc", -3.5e-2, "a_b%", "a_b%") +f4 BETWEEN f6 AND f3 +f4 IN (3.14) +f5 = NULL +f1 <= -.5 +() +f5 NOT CONTAINS '' +has(f6, -.5) +f4 BETWEEN AND '' +f6 BETWEEN "%wild%" AND f4 +f5 BETWEEN f6 AND -.5 +hasAny(f3, "\t") +f6 BETWEEN 'O''Reilly' +f5 REGEXP "\t" +f3 NOT ILIKE "abc" +f5 BETWEEN "\n" AND 6.02e23 +f2 = 6.02e23) +f5 NOT CONTAINS "abc" +hasAll(f2, f4) +f2 IN ("%wild%", ' ', "xyz%", f4) +f3 BETWEEN 'O''Reilly' AND 0.0000000001 +f3 LIKE +hasToken(f4, ' ') +f2 = 'unclosed +hasAll(f3, -.5) +(f2 BETWEEN 10 AND 0.0000000001) AND (f1 <= "%wild%") +f6 != 0 +f2 IN () +f5 BETWEEN -1 AND 1e10 +f1 BETWEEN "\t" AND f5 +(f1 < -2.7) OR (f4 IN (f5, -.5, f6, f3, "hello world")) +f3 != f3 +(f6 > 'test') AND (f5 BETWEEN "\t" AND f1) +f4 == 1e10 +f4 BETWEEN "\n" +(f3 > "abc") AND ((f3 ILIKE "%wild%") OR (((f3 <> 0.0000000001) AND (f4 != f3)) OR (f6 NOT CONTAINS ''))) +f2 = 'unclosed +f2 BETWEEN "a_b%" AND f2 +f6 BETWEEN f5 AND 3.14 +f4 = +f6 <> -10 +f6 IN (10) +(f1 = TRUE +f4 NOT LIKE "\t" +f5 = +f4 = 'unclosed +f6 BETWEEN AND .5 +(f2 = "\t") OR (f5 BETWEEN 999999999999 AND 10) +f2 IN ("a_b%", "a_b%", "abc", 'O''Reilly', 6.02e23) +f5 BETWEEN AND 0.0000000001 +() +f3 BETWEEN 6.02e23 AND f3 +f5 IN () +f3 IN [1 2 3] +hasToken(f2, 999999999999) +(f3 = "abc" +NOT +f4 IN [1 2 3] +hasToken(f6) +f4 BETWEEN "%wild%" AND "xyz%" +() +AND f1 = -10 +hasAll(f4, 10) +(f6 NOT CONTAINS "abc") AND (hasToken(f5, 0)) +(has(f3, f6)) OR (f1 BETWEEN -2.7 AND 6.02e23) +hasAll(f5, ' ') +f1 NOT REGEXP "%wild%" +f6 BETWEEN 0.0000000001 AND 'O''Reilly' +f6 = 3.14 +f5 NOT CONTAINS "abc" +(f4 <= "%wild%") AND (hasAny(f1, 999999999999)) +f6 = +(f5 = "\n" +f2 BETWEEN -2.7 +hasToken(f5, "a_b%") +f4 IN (0.0000000001, ' ', "\n", f1) +hasToken(f6, 3.14) +f3 BETWEEN 6.02e23 +f1 < f2 +f1 IN (3.14, 1e10) +hasToken(f5, -2.7) +f5 IN ("a_b%") +AND f1 = TRUE +f4 IN (-10, -3.5e-2) +f3 NOT CONTAINS "xyz%" +f6 BETWEEN f5 AND -1e-10 +f3 BETWEEN 0 AND "\t" +hasAll(f4, f2) +(f3 BETWEEN "a_b%" AND -1e-10) OR (f2 IN ("a_b%", 'test', f5, "abc")) +f4 IN (.5, -1e-10, 1e10) +hasToken(f1, 999999999999) +f6 IN (TRUE) +f6 BETWEEN 'O''Reilly' AND "abc" +f6 BETWEEN f1 AND "\n" +hasToken(f4, f6) +f6 BETWEEN AND TRUE +((f4 IN (0)) AND (f2 IN (0.0000000001, f2, '', "\t", f3))) AND (f1 IN (-2.7, 'test', TRUE, 3.14)) +(f3 IN (f3, 999999999999, f4)) OR (f1 NOT CONTAINS ' ') +f1 IN (1) +hasAny(f6) +(f6 = f2 +f2 BETWEEN "hello world" AND -10 +hasAll(f2, "a_b%") +f3 >= "a_b%" +has(f4, ' ') +f5 ILIKE "\t" +f5 IN (3.14, -.5, "a_b%", .5, "abc") +f3 BETWEEN 6.02e23 AND f6 +hasToken(f2, f1) +((f5 BETWEEN -3.5e-2 AND f5) OR (f2 BETWEEN "a_b%" AND f2)) AND (f1 IN (-.5, -1e-10, "\n", f6)) +f6 NOT CONTAINS "%wild%" +f4 ILIKE "xyz%" +f3 BETWEEN -1 AND "hello world" +f2 IN (f5, f6, .5, .5) +has(f3, f2) +f1 NOT ILIKE 'O''Reilly' +f1 IN [1 2 3] +((f5 BETWEEN -.5 AND 999999999999) AND (f5 > "hello world")) AND (f6 IN ('test', "hello world", f6)) +AND f6 = -.5 +f6 BETWEEN "\t" AND f5 +((f4 NOT LIKE "\t") AND (f5 REGEXP "a_b%")) OR (f3 LIKE "xyz%") +f6 NOT REGEXP "xyz%" +() +hasAll(f4, 10) +(f1 != f2) OR (f4 <> -.5) +f3 BETWEEN AND -3.5e-2 +f6 IN (f1, 3.14) +f2 >= -10 +f5 BETWEEN f2 AND 0 +f1 IN (-.5, f4, -10, 0.0000000001) +(f3 > 'O''Reilly') AND (f2 ILIKE "xyz%") +f5 != f2 +f6 BETWEEN 6.02e23 AND TRUE +f2 NOT LIKE "xyz%" +f5 <= ' ' +f3 = 'unclosed +hasToken(f2, 'test') +f4 BETWEEN "\n" AND .5 +(f5 = '') OR (f4 BETWEEN TRUE AND '') +f4 BETWEEN -10 AND 1 +f3 <> -1 +f4 NOT +f5 <> 6.02e23 +f5 NOT LIKE 'O''Reilly' +(f2 == f1) AND (f5 LIKE 'O''Reilly') +(f1 BETWEEN 0 AND -.5) AND (f4 BETWEEN ' ' AND "xyz%") +f6 BETWEEN "hello world" AND .5 +NOT +f5 ILIKE "a_b%" +f5 == 0.0000000001 +f1 = "abc" +f4 IN (1e10, -1) +((f3 BETWEEN "abc" AND TRUE) OR (f1 IN ('', .5, f6, 10, 1e10))) AND (hasToken(f1, -10)) +f3 IN [1 2 3] +f6 = 0) +has(f3, "%wild%") +((f1 IN ("abc", TRUE)) OR (f2 < 3.14)) AND (f5 BETWEEN TRUE AND '') +(hasAny(f1, "\t")) AND (has(f5, f1)) +f2 BETWEEN 'test' AND "xyz%" +f4 IN [1 2 3] +f2 >= f3 +f5 > 999999999999 +f3 NOT LIKE "xyz%" +((hasAny(f1, "a_b%")) AND (f6 <> ' ')) OR (hasAll(f1, "xyz%")) +f2 LIKE +f2 BETWEEN -1e-10 AND "\t" +(f5 < -10) AND (f2 >= -.5) +f1 IN (f3) +hasToken(f4, f5) +(f3 BETWEEN 999999999999 AND 1e10) OR (f4 IN (-1, "abc")) +f4 NOT CONTAINS "abc" +f2 CONTAINS 'test' +has(f2, "\n") +f3 BETWEEN "\n" +f3 = f4 +hasAny(f2, -2.7) +f3 BETWEEN 10 AND 0.0000000001 +f3 ILIKE "hello world" +f6 IN (1, ' ', 6.02e23) +f4 BETWEEN 'O''Reilly' AND 1e10 +f1 <> 0.0000000001 +f1 NOT REGEXP "\n" +f3 BETWEEN 1e10 AND "a_b%" +f5 BETWEEN 6.02e23 AND .5 +f3 === "\t" +f6 NOT CONTAINS "\t" +((f2 IN (1e10, "\n", 'O''Reilly', 'test', -.5)) AND (((f4 = 1e10) OR (f5 <> -2.7)) OR (f3 BETWEEN -10 AND "abc"))) OR (f2 IN ("%wild%")) +f2 IN (-10, ' ') +f6 = 10 +(f4 BETWEEN f5 AND "\n") OR (has(f4, f4)) +f5 NOT +f1 = +(hasAny(f4, 10)) AND (f2 IN (TRUE, "xyz%")) +f2 IN (10) +hasAny(f4, 10) +f1 REGEXP "\t" +f2 NOT LIKE "abc" +(f4 == 'O''Reilly') OR (f4 REGEXP "xyz%") +f2 NOT REGEXP "%wild%" +f1 REGEXP ' ' +f3 BETWEEN 10 AND 6.02e23 +() +f5 NOT ILIKE ' ' +f6 LIKE '' +hasAny(f5, .5) +f4 = 3.14) +(hasAll(f3, -10)) AND (f5 CONTAINS '') +f5 IN (0, TRUE, .5, TRUE) +has(f5, -2.7) +f3 > TRUE +(hasToken(f5, -1)) AND (f4 IN ("\t", -1)) +f6 IN (10, f4) +AND f3 = "%wild%" +(f3 IN (3.14, -1e-10, f2, 999999999999)) OR ((has(f3, f6)) AND (f3 <= -.5)) +f5 BETWEEN AND 1 +hasToken(f6) +hasAny(f4, f4) +f3 IN ("xyz%", 3.14) +f1 ILIKE "\n" +f2 = NULL +f6 OR OR "hello world" +f4 BETWEEN 'O''Reilly' AND "\t" +f5 > 'test' +f4 == "%wild%" +(f6 = "abc" +(((f3 IN ("\t", "\n", -1, -1e-10)) OR ((f2 >= ' ') OR (f2 != 'test'))) AND (f2 REGEXP '')) AND (((f3 BETWEEN 0.0000000001 AND 'O''Reilly') OR (f3 IN ('test', f3, .5))) OR (f6 == f4)) +f5 BETWEEN 10 +hasAny(f4, "a_b%") +f2 NOT +f5 BETWEEN -3.5e-2 AND f5 +f1 BETWEEN AND -.5 +(f3 <> 1) AND ((f2 BETWEEN ' ' AND 'O''Reilly') AND (f4 < -3.5e-2)) +f3 < f3 +hasAll(f5, f1) +hasAny(f2) +f2 BETWEEN 1 AND f6 +f6 IN ("\n") +(f3 = TRUE +f4 IN (999999999999, 0) +((f5 IN (-.5, TRUE, -2.7, f6, .5)) OR (f1 >= -1e-10)) AND (f5 IN (' ', TRUE)) +f5 >= 999999999999 +f3 NOT ILIKE "abc" +f1 >= 6.02e23 +f5 BETWEEN AND -10 +f1 BETWEEN 'test' AND '' +f3 OR OR -.5 +f5 IS NULL +f6 NOT CONTAINS "%wild%" +f1 BETWEEN -.5 AND 10 +AND f2 = TRUE +f1 CONTAINS ' ' +f6 IN (0, 0.0000000001, "%wild%") +(hasToken(f1, "abc")) OR (f6 IN (6.02e23, 10, 0, -3.5e-2)) +f4 ILIKE "xyz%" +has(f3) +f5 NOT CONTAINS "xyz%" +() +f5 < f6 +(f3 NOT REGEXP "abc") AND (f3 NOT CONTAINS "\t") +f1 NOT LIKE "\t" +f3 IN ('', -2.7, -1) +f4 = 'unclosed +f6 BETWEEN "%wild%" AND -2.7 +f1 IN [1 2 3] +hasAny(f2, -.5) +(hasToken(f3, TRUE)) AND (f4 IN ('test', f2, -.5)) +hasAll(f6, "a_b%") +hasAny(f6, -1e-10) +((((f2 > .5) OR (f5 >= TRUE)) OR (f1 NOT LIKE "abc")) OR (f1 BETWEEN 10 AND -1e-10)) OR (f3 BETWEEN "xyz%" AND 6.02e23) +hasAll(f6) +((f4 <= TRUE) OR (has(f1, "xyz%"))) AND (hasAny(f4, '')) +NOT +f4 < "xyz%" +has(f3, "%wild%") +f1 BETWEEN 6.02e23 AND "a_b%" +((f5 NOT REGEXP "xyz%") OR (hasAny(f2, -10))) AND (f5 CONTAINS "xyz%") +f6 BETWEEN "%wild%" AND "%wild%" +f5 BETWEEN f4 AND 0 +f4 IN ("abc", -1) +f4 IN ("\n") +(f5 > f6) OR (f1 REGEXP "\t") +has(f1, -3.5e-2) +f5 IN () +f1 BETWEEN f3 AND "%wild%" +f1 != "xyz%" +hasAll(f2, TRUE) +(f3 LIKE "%wild%") AND (has(f1, -1)) +f3 == 'test' +() +(hasAny(f3, 'O''Reilly')) OR (f5 != .5) +hasToken(f5, 3.14) +((f4 BETWEEN f6 AND 6.02e23) AND (f2 BETWEEN "xyz%" AND -1e-10)) AND (f6 BETWEEN 10 AND f3) +f3 == "xyz%" +(f3 NOT REGEXP "hello world") AND (f3 == -1e-10) +f3 REGEXP "\n" +hasAll(f3, f1) +f2 IN (f1, "hello world") +f2 === f3 +f3 != -1e-10 +((f5 = "\t") AND (f5 < f4)) AND (f4 IN ('test', -.5)) +hasToken(f1, '') +(hasAny(f6, f6)) AND (f2 ILIKE "\n") +f6 NOT +f6 <> '' +f1 LIKE "\t" +(((hasAny(f5, "\n")) OR (f5 IN ("xyz%"))) OR (f5 BETWEEN "\t" AND ' ')) OR (f4 LIKE '') +f4 OR OR 6.02e23 +f6 <> TRUE +hasToken(f6) +hasAny(f5, -.5) +f5 != 10 +f5 BETWEEN "\t" AND 1 +f6 = 0.0000000001) +hasAll(f2, -3.5e-2) +f4 IN (-10, 0.0000000001, "\n", "\n", -.5) +f5 BETWEEN f6 AND TRUE +AND f5 = 1e10 +(f5 = 0.0000000001 +(hasToken(f1, -.5)) OR (f2 NOT LIKE 'O''Reilly') +hasAll(f4, -1e-10) +(has(f5, 0)) OR (((f3 NOT LIKE "hello world") OR ((f3 < f1) OR (f5 < "\t"))) AND (f5 NOT CONTAINS "hello world")) +hasAll(f4, 'O''Reilly') +((((f4 != -2.7) AND (f5 <= "%wild%")) AND (f4 BETWEEN 0 AND 3.14)) AND (f5 CONTAINS "xyz%")) AND (f2 IN (3.14, -1, "\n")) +f4 = 'unclosed +hasAny(f5, -3.5e-2) +f2 IN (-3.5e-2) +f4 IN ("\t", "\t", -1e-10) +(f1 < ' ') OR (f2 IN (-1e-10, f3, "\t", 0, "xyz%")) +has(f1, f6) +f5 < "xyz%" +f5 NOT CONTAINS 'test' +has(f4) +f4 IN () +f4 BETWEEN "a_b%" AND ' ' +hasAny(f3, -1e-10) +f2 BETWEEN "abc" AND "xyz%" +f2 BETWEEN ' ' AND 6.02e23 +f1 IN [1 2 3] +f4 = NULL +f1 = +f4 BETWEEN -2.7 AND "a_b%" +f2 IN ("xyz%", "abc", "hello world", f4, "%wild%") +f4 NOT ILIKE "%wild%" +f3 IN ("hello world", f2, 1e10, -.5) +AND f6 = -2.7 +f2 <= f5 +(hasAll(f6, f5)) OR (f5 IN (10, "\t", -2.7, -3.5e-2, 10)) +f4 IN (f2) +f3 IN ("xyz%", ' ', 1) +f6 IN (-1e-10, 1e10, 'test', ' ', "a_b%") +f6 NOT CONTAINS "xyz%" +f4 IN (-3.5e-2, "%wild%") +hasToken(f5, -10) +f2 IN ("a_b%", 'test', "hello world") +f3 NOT CONTAINS "a_b%" +hasToken(f4, -3.5e-2) +f2 <> "xyz%" +() +f5 NOT LIKE "xyz%" +f1 BETWEEN "\n" AND 'O''Reilly' +(f6 ILIKE "hello world") AND (f1 > "a_b%") +f4 = NULL +f4 < '' +f3 BETWEEN 0 AND 0 +f3 BETWEEN 'test' AND f4 +(f3 LIKE "%wild%") OR (has(f2, '')) +hasToken(f4, "abc") +(f4 NOT ILIKE 'O''Reilly') AND (f6 NOT ILIKE '') +f5 IN (,) +f5 NOT CONTAINS 'O''Reilly' +f2 NOT REGEXP "abc" +hasAny(f1, -2.7) +f6 BETWEEN f2 AND 0 +f1 BETWEEN f5 AND 999999999999 +f4 IN (f3, -1, "%wild%") +f1 BETWEEN "hello world" AND "\t" +f2 NOT ILIKE 'O''Reilly' +f1 LIKE +f4 BETWEEN "\n" AND 'O''Reilly' +f2 = 'unclosed +f2 = +f5 BETWEEN "abc" +f6 BETWEEN -2.7 +f6 BETWEEN AND ' ' +f5 REGEXP '' +f5 BETWEEN 1e10 AND ' ' +f3 ILIKE "xyz%" +f3 REGEXP '' +f6 <= 1e10 +f5 IN (,) +(f1 == -1) AND (f1 BETWEEN -1e-10 AND -2.7) +f1 <= f5 +(f1 IN (0.0000000001, -10, 0)) AND ((f4 = '') OR (f6 IN ("hello world"))) +(f1 IN (-2.7, -2.7)) AND (hasAny(f4, -1e-10)) +f5 BETWEEN 1e10 AND TRUE +(f6 IN (f1, -2.7, -3.5e-2, 1e10)) OR (hasAny(f3, f2)) +() +NOT +f1 IN (0.0000000001, 999999999999, "a_b%") +(hasAny(f5, -2.7)) AND (f3 ILIKE "hello world") +hasAny(f2, "xyz%") +f1 IN (,) +has(f4, 10) +f3 IN (,) +f5 IN (6.02e23, "a_b%", f6, 0.0000000001, 6.02e23) +(f5 NOT LIKE 'test') OR (f4 IN (-.5, "%wild%")) +has(f6, "\t") +f5 CONTAINS "hello world" +f4 = "hello world") +has(f1, TRUE) +f2 IN (' ', 1, 'O''Reilly') +f5 IN (6.02e23, f2, -1, 0.0000000001) +f2 IN (.5) +f5 BETWEEN 6.02e23 AND 'test' +(f4 IN (1, "%wild%", 999999999999, 999999999999)) AND (hasAll(f5, f4)) +f6 BETWEEN 1 AND "\n" +(f3 != 10) OR (f6 BETWEEN "hello world" AND 10) +f1 <= "hello world" +(f1 < "\n") AND (f5 NOT LIKE "a_b%") +has(f5, 1e10) +f5 LIKE "abc" +hasAll(f3, f5) +f1 BETWEEN ' ' AND ' ' +f2 IN ("hello world", 10, "xyz%", "\n", 1) +f3 = 0.0000000001) +f4 BETWEEN 'O''Reilly' AND "%wild%" +f2 BETWEEN -.5 AND -1e-10 +f3 BETWEEN "a_b%" +f2 = 'unclosed +f1 BETWEEN "hello world" AND 0.0000000001 +f6 BETWEEN 1 AND ' ' +f2 NOT CONTAINS "\t" +f4 NOT CONTAINS "hello world" +f1 BETWEEN TRUE AND 0 +f4 IS NULL +f4 IS NULL +(f1 = 'test' +f3 IN () +f2 != "\t" +f1 ILIKE ' ' +(f1 REGEXP "a_b%") AND ((f1 == f2) OR (f5 <= f6)) +f4 BETWEEN "\n" AND -1 +f3 IS NULL +f1 BETWEEN -.5 AND -1e-10 +f1 IN (f5, f5, 0, f5) +hasToken(f6, f3) +f5 = "abc" +f3 BETWEEN AND 999999999999 +hasAll(f3, 3.14) +f2 ILIKE "\t" +((hasAny(f5, f3)) OR (f1 < "a_b%")) OR (f1 CONTAINS '') +f2 NOT LIKE 'O''Reilly' +f5 NOT ILIKE 'test' +f5 == f4 +hasAll(f6, f1) +f3 IN (' ', -.5, f5, f5, f5) +f5 = 'unclosed +f4 NOT +f3 IN [1 2 3] +f2 <> "%wild%" +f6 IS NULL +AND f4 = 'O''Reilly' +f3 <= -3.5e-2 +f2 NOT LIKE 'O''Reilly' +f6 IN ("hello world") +f2 NOT REGEXP ' ' +f3 IS NULL +f5 ILIKE 'test' +f6 REGEXP "a_b%" +f5 != 999999999999 +f4 NOT LIKE "abc" +f6 == "%wild%" +f1 < 1 +f6 IN ("a_b%", -1e-10) +f5 IN (f2, .5, "hello world") +f1 IN (f6) +hasAll(f3, "hello world") +(hasAny(f2, -10)) OR (hasToken(f3, 10)) +f6 LIKE "xyz%" +f4 <= 1e10 +hasToken(f6, ' ') +f6 BETWEEN 3.14 AND f3 +f6 >= -2.7 +((f2 NOT ILIKE "hello world") OR (f4 IN ("%wild%", "a_b%"))) OR (f1 NOT LIKE ' ') +f4 ILIKE 'O''Reilly' +() +hasAny(f4, "abc") +f4 NOT LIKE "a_b%" +(f4 NOT REGEXP '') AND (f6 BETWEEN -10 AND f5) +hasAll(f4, -2.7) +f2 IN () +f5 BETWEEN 0 AND 6.02e23 +f5 REGEXP "%wild%" +f2 NOT ILIKE "\t" +f3 IN (,) +has(f1, ' ') +f1 IN (f1, "\t") +(f4 CONTAINS '') AND ((f2 NOT ILIKE '') OR (f4 IN ('test', -.5))) +f6 = 6.02e23 +f6 CONTAINS "hello world" +(f6 CONTAINS "\n") AND (f5 CONTAINS '') +f4 NOT +hasAll(f5, -1) +NOT +hasAny(f1, f4) +(f6 < 10) OR (hasToken(f1, f3)) +f5 NOT REGEXP "%wild%" +f3 IN (-1, 'O''Reilly') +has(f3, f4) +hasAll(f1, 1) +f2 == "hello world" +hasAll(f3, "xyz%") +f5 != 6.02e23 +f2 IN ("abc", f5, 1e10) +f1 IS NULL +f2 BETWEEN ' ' AND 10 +f1 != f3 +f4 IN (0.0000000001, "\n", "\n", f4, "xyz%") +f1 REGEXP ' ' +f5 IN (-2.7) +f6 IN [1 2 3] +(f6 BETWEEN -1e-10 AND "%wild%") AND (f5 IN (0)) +f1 = 'unclosed +has(f1, 999999999999) +f6 NOT +((f4 IN ("xyz%", -3.5e-2, -10)) OR (f4 <= "%wild%")) AND (f3 IN (f2, -10)) +f5 BETWEEN "\t" AND 1e10 +f2 <= 999999999999 +f4 < -2.7 +f5 < f2 +f2 LIKE "abc" +f2 IN ("\t", 0, f6) +f4 CONTAINS "%wild%" +(f1 = "%wild%" +(f5 IN (3.14, f3, 'test')) AND (f5 BETWEEN -1 AND "\n") +f6 IN (-2.7, ' ', f5, 3.14) +f4 IN () +f6 BETWEEN AND 'O''Reilly' +f6 BETWEEN f3 AND 6.02e23 +f1 BETWEEN -1 AND 6.02e23 +f4 = "abc" +f1 BETWEEN 1e10 AND 10 +f1 = 'unclosed +(f3 NOT LIKE "\t") AND (f2 IN (1e10)) +f3 != TRUE +f5 IN (f2, "abc") +f1 = +f6 < TRUE +f6 ILIKE 'test' +f5 IN (TRUE, "abc", 1e10, -1) +(hasAll(f1, ' ')) OR (f5 <> -1e-10) +hasAll(f1, f6) +(f3 BETWEEN 10 AND -1e-10) AND (f6 BETWEEN f4 AND f5) +(f6 > 'test') AND (f3 NOT REGEXP "\n") +f4 LIKE +f6 BETWEEN f2 AND "a_b%" +(f2 IN ("a_b%")) AND (f2 <= -1e-10) +f1 BETWEEN f4 AND f5 +f2 NOT ILIKE "\n" +f3 ILIKE ' ' +(f2 IN (-.5, -1, 10)) OR (f1 BETWEEN f3 AND f3) +f2 IN (-3.5e-2, -3.5e-2, 0.0000000001) +hasAny(f4, f4) +f3 IN [1 2 3] +f4 IS NULL +f5 = TRUE +f2 IS NULL +f6 = +f1 BETWEEN f1 +f5 > 10 +f4 IN (-1, '', '', 1e10, -10) +f2 BETWEEN "%wild%" AND "abc" +((((f3 > "hello world") OR (f3 = -10)) OR (f4 IN (f6, .5, f4, 'test'))) OR (has(f3, -1e-10))) OR (f1 <= 1e10) +f1 != "hello world" +f4 BETWEEN 6.02e23 AND 999999999999 +(f4 IN (-.5, f4, ' ', TRUE, f2)) AND (f2 IN (1)) +f2 IN [1 2 3] +f2 IN (0.0000000001, f4, '') +f1 === 'test' +f6 NOT +f5 BETWEEN -1 AND "a_b%" +AND f2 = f2 +f3 IN [1 2 3] +f3 NOT LIKE ' ' +f2 BETWEEN 0.0000000001 +(hasAny(f5, "hello world")) AND (f1 >= 3.14) +f4 OR OR "\t" +f3 > ' ' +f5 NOT LIKE 'test' +f6 CONTAINS ' ' +((f5 BETWEEN 3.14 AND 6.02e23) AND (f4 IN ("%wild%", -10, "\n", 3.14, 0))) OR ((f2 NOT CONTAINS "\n") AND (f2 BETWEEN "\n" AND 'test')) +f3 < f5 +f2 LIKE "abc" +f6 IN (.5, 'test', TRUE, 1e10) +(f1 BETWEEN .5 AND "hello world") OR (f5 > f4) +f6 IN (.5, '', f6, 1) +f2 NOT +(f3 < -10) AND ((f5 != "a_b%") OR ((f6 IN (-2.7, ' ', 6.02e23)) AND (f5 BETWEEN 'test' AND ''))) +() +has(f5, TRUE) +f1 != 1 +f6 IN (-.5, f2, "%wild%") +f1 IN (0.0000000001) +((f2 IN (-.5, 3.14)) OR ((f1 >= f1) AND (f1 BETWEEN -1 AND f6))) AND (f1 REGEXP "abc") +(f1 <> -1e-10) AND ((f1 != "hello world") OR (f2 IN ("abc", f6, -10, -2.7, 0))) +(f2 IN (0, -2.7, 999999999999, f6, f4)) OR ((f4 BETWEEN -2.7 AND 1e10) OR (f4 NOT ILIKE '')) +f3 == f3 +f4 BETWEEN '' AND 'test' +f1 > f5 +f4 IN ('O''Reilly', f2, 0.0000000001, TRUE, 'test') +(f4 = f5 +(f1 != "\t") OR ((f6 != -10) OR (f3 BETWEEN -1e-10 AND "\t")) +f6 NOT CONTAINS 'O''Reilly' +AND f5 = "\t" +f1 BETWEEN AND .5 +NOT +f4 BETWEEN AND -1e-10 +f1 === "a_b%" +f6 IN (f5, f1) +(f1 NOT CONTAINS "\t") OR (f5 ILIKE '') +f4 IN ("\n", ' ', 'test', "a_b%", 10) +f2 > ' ' +f5 OR OR ' ' +f2 BETWEEN "abc" AND 'O''Reilly' +(f1 IN (-3.5e-2, 1, "a_b%")) AND (f6 CONTAINS "\n") +f3 LIKE ' ' +f4 IN ("\n", "a_b%", -2.7, f5, -2.7) +f3 BETWEEN f5 AND f6 +() +f1 IN ("\t", 0.0000000001, 3.14) +hasAll(f1, "hello world") +hasToken(f4, 999999999999) +f6 < f6 +f4 = +hasToken(f2, 999999999999) +(f6 IN (f1, 10, -1e-10, '', f3)) AND (f2 BETWEEN 'O''Reilly' AND f1) +f6 <> TRUE +f6 IN ("xyz%", "abc") +f2 <> 1 +f4 IN (f1, f1, -1, -1e-10) +f1 BETWEEN "xyz%" AND "hello world" +f6 BETWEEN -1 AND f5 +AND f1 = "a_b%" +has(f6, "xyz%") +f2 == "a_b%" +hasToken(f6, "abc") +f2 IN [1 2 3] +has(f3, "a_b%") +f6 NOT ILIKE 'O''Reilly' +f2 NOT +f6 IN (.5, "abc", f2) +f3 REGEXP "xyz%" +f2 IN () +f4 IN () +hasAll(f6, f3) +f6 != f2 +hasAll(f2) +(hasToken(f6, "a_b%")) OR (f6 ILIKE 'O''Reilly') +hasAll(f3, -1e-10) +f2 IN ("\n", 0.0000000001, 1, 1) +f6 = "\t") +f1 BETWEEN '' AND -3.5e-2 +f1 NOT +f4 ILIKE "%wild%" +f5 IN (,) +f2 BETWEEN 3.14 +f4 BETWEEN -10 +f4 OR OR "\n" +hasAny(f1, "xyz%") +f3 NOT CONTAINS "hello world" +(f2 REGEXP "xyz%") OR (f2 >= "hello world") +f3 REGEXP "\n" +f3 = 'unclosed +f2 BETWEEN -1 AND f4 +(f1 NOT CONTAINS "abc") OR (f6 IN (f5, f3, f5, .5, 6.02e23)) +f2 < 1e10 +f6 = NULL +f1 IN (.5, "\t", '') +f3 != 1 +(f4 REGEXP "\n") AND (f1 IN (f2, "a_b%", -.5, 999999999999, 10)) +hasToken(f3) +f4 LIKE +f6 REGEXP "\t" +f6 IN ('') +f4 CONTAINS ' ' +has(f4, f1) +has(f5) +(f5 NOT LIKE 'O''Reilly') OR (f1 NOT CONTAINS 'O''Reilly') +f6 BETWEEN 'O''Reilly' AND "abc" +f5 IN (-10) +f5 NOT +has(f1, f3) +f4 BETWEEN -1 AND 3.14 +(f5 IN (f2, "xyz%", "%wild%", 1e10)) AND (f1 BETWEEN "xyz%" AND -2.7) +f1 <= .5 +(f6 CONTAINS "\t") OR (f5 IN ('', "\t", 'O''Reilly', 3.14, -.5)) +f1 BETWEEN 1 AND -1e-10 +AND f2 = 6.02e23 +f1 != 6.02e23 +f4 NOT CONTAINS "abc" +f5 = -2.7) +f4 BETWEEN AND 'test' +f5 NOT REGEXP '' +(f1 NOT CONTAINS "\n") OR (f6 CONTAINS "abc") +(f3 NOT CONTAINS 'test') AND (hasAll(f6, f3)) +f6 IN (TRUE, '', f4, -3.5e-2) +f6 IN (f4) +f2 BETWEEN f5 AND -2.7 +f4 === 999999999999 +has(f1, "%wild%") +has(f2, 0.0000000001) +f3 IN ("hello world") +() +f2 == 999999999999 +AND f2 = f2 +f4 BETWEEN TRUE AND 0 +f3 REGEXP '' +f3 IN ('', "abc", 6.02e23, TRUE) +f2 = +has(f1, "%wild%") +f4 BETWEEN 10 AND "\t" +f6 = +f5 IN (,) +hasToken(f3, -.5) +hasAny(f3, 3.14) +f5 IN (-3.5e-2, 1, 1) +f5 NOT +f1 IN (.5) +has(f6, f3) +f6 BETWEEN 0.0000000001 AND ' ' +(hasAny(f6, "\n")) AND (f3 <> '') +f6 BETWEEN f1 AND TRUE +f4 != f4 +f3 <= "\t" +AND f5 = f1 +f2 NOT LIKE 'test' +f6 != '' +f1 OR OR -1 +f3 IN (f5, 1, 6.02e23) +has(f4, 'O''Reilly') +f2 = NULL +hasAll(f4, "\n") +(f2 BETWEEN f6 AND f3) OR ((f6 BETWEEN "a_b%" AND 3.14) OR (f2 CONTAINS "a_b%")) +(((f6 BETWEEN 1 AND 'O''Reilly') AND ((f1 <> 10) AND (f4 = .5))) AND (f3 <> "abc")) OR (f5 > "\t") +f1 NOT REGEXP "%wild%" +f5 > "\n" +f1 != 'test' +f1 IN ('O''Reilly', TRUE) +f5 > 1e10 +f3 <= -2.7 +f3 REGEXP "%wild%" +f1 LIKE +f4 IN (TRUE, -10, f4, 1, TRUE) +f3 == "abc" +f6 OR OR f6 +f2 BETWEEN f1 AND f2 +f5 REGEXP "\t" +f1 IN (,) +(hasAll(f1, 3.14)) OR ((f6 BETWEEN "hello world" AND f1) AND (hasAll(f5, "%wild%"))) +(f2 IN ("%wild%", 999999999999, f3)) OR (f6 != 0) +f1 != TRUE +(f4 NOT LIKE "\t") AND (hasToken(f1, .5)) +f5 NOT REGEXP "%wild%" +(f4 > -1e-10) OR (f2 = "hello world") +(f1 BETWEEN 'O''Reilly' AND 0.0000000001) AND (f3 LIKE 'O''Reilly') +(has(f2, -1e-10)) OR (f3 >= -3.5e-2) +(hasAny(f6, -10)) AND ((f3 BETWEEN 0.0000000001 AND "hello world") AND (((f5 < f3) AND (f4 = "a_b%")) OR (f3 NOT LIKE "\t"))) +f3 LIKE 'test' +f5 NOT CONTAINS "a_b%" +f3 BETWEEN 3.14 AND 1 +f2 LIKE +f1 NOT REGEXP 'O''Reilly' +f2 NOT REGEXP "abc" +f5 BETWEEN AND "abc" +(f1 BETWEEN f2 AND 10) OR (hasToken(f6, -10)) +f5 = 'test' +f4 LIKE +(f5 NOT REGEXP "\n") AND (f1 = -10) +f5 BETWEEN -.5 AND f6 +f4 NOT CONTAINS "hello world" +f1 != 'O''Reilly' +hasAll(f6, 'O''Reilly') +hasAny(f2, f4) +f3 IN (0, 6.02e23, 10) +hasToken(f1, ' ') +f6 BETWEEN -1e-10 AND -10 +((f5 NOT REGEXP "\n") OR (hasToken(f3, "%wild%"))) AND (f4 = f5) +f6 <> 0.0000000001 +f2 BETWEEN f4 AND "%wild%" +f4 = +f3 BETWEEN f2 AND -1e-10 +f1 IN (-1e-10) +f2 BETWEEN "xyz%" AND 1e10 +hasAny(f2, -1e-10) +f1 IN (-.5) +f3 <> 'O''Reilly' +f3 IN (f1, -10) +f2 BETWEEN -1e-10 AND "\n" +f5 IN ("abc", -1, -1e-10, 'test') +f3 = NULL +f5 === f6 +f5 IN (f5, f4, "hello world", "\t") +has(f6, 999999999999) +f4 BETWEEN "abc" AND "\t" +f4 ILIKE "a_b%" +f2 == 'test' +f2 IN (-1, f3, f6, "%wild%", f1) +f4 NOT REGEXP ' ' +f1 ILIKE ' ' +(f1 IN (f6, f6, 3.14)) OR (f1 BETWEEN .5 AND -10) +hasAll(f2, -1e-10) +f3 IN (-1e-10, -.5, "xyz%") +f3 IN ("\t", "\t", 0) +hasToken(f2, 0.0000000001) +(f2 NOT CONTAINS ' ') OR (f3 <> f1) +f6 IN (,) +f1 BETWEEN TRUE +hasAny(f1, 6.02e23) +f3 LIKE +f6 REGEXP "xyz%" +(f1 BETWEEN f5 AND 1) AND (f1 BETWEEN "\n" AND 'test') +f6 IN (-10, f3, 10, '', 0) +f4 ILIKE "xyz%" +f4 NOT LIKE "%wild%" +NOT +f3 NOT ILIKE "abc" +f4 IN () +f3 REGEXP "%wild%" +f4 BETWEEN "abc" AND 1e10 +f6 === "\n" +(f6 IN (999999999999)) OR (f5 NOT LIKE "a_b%") +f5 > TRUE +f2 IN (-1, f3, 1, -10, f3) +(f4 BETWEEN -1 AND 0) OR (f6 != "a_b%") +f4 NOT CONTAINS "xyz%" +((f4 BETWEEN "hello world" AND -3.5e-2) AND (f5 == 1e10)) OR (f5 <= -3.5e-2) +f6 < 6.02e23 +f2 BETWEEN -1 AND 999999999999 +f6 == "\n" +f1 LIKE "abc" +f2 = +f6 BETWEEN -10 AND 0.0000000001 +() +hasToken(f2, f1) +f1 BETWEEN ' ' AND "a_b%" +f6 NOT REGEXP "abc" +f2 NOT LIKE "\t" +() +f5 BETWEEN "\n" +NOT +f2 = .5 +(f5 BETWEEN 3.14 AND 10) AND (f4 IN (-1e-10, 6.02e23, "\n")) +f2 >= "%wild%" +f6 IN (TRUE) +f1 IN ("%wild%", "%wild%") +hasAny(f1, 0.0000000001) +f5 NOT REGEXP "hello world" +f1 >= -10 +f1 IN () +f3 ILIKE 'test' +f5 BETWEEN "abc" AND 'O''Reilly' +f6 NOT ILIKE 'O''Reilly' +NOT +f2 NOT CONTAINS "abc" +f4 IS NULL +hasToken(f5, -10) +(f2 BETWEEN -1e-10 AND 0) AND (hasAll(f6, f5)) +f4 NOT REGEXP "%wild%" +(f4 = .5) OR (hasAny(f2, f1)) +hasAll(f6) +f6 IN (6.02e23) +f6 ILIKE 'O''Reilly' +hasAny(f6, 0) +f6 <= "\t" +f5 <> f1 +f3 REGEXP '' +f6 = 'unclosed +NOT +f6 IN (-3.5e-2, 1e10, '', 1e10) +f4 BETWEEN 1e10 AND f6 +f4 IS NULL +hasAny(f1, 3.14) +f1 REGEXP "xyz%" +hasToken(f2, f5) +f1 = "hello world" +f1 BETWEEN "\n" +f3 IN ("a_b%", "abc", '') +f5 LIKE "a_b%" +f1 OR OR .5 +(f5 CONTAINS "\t") AND (f6 > 1) +f6 > f2 +f4 NOT LIKE "a_b%" +f6 NOT LIKE "%wild%" +f6 BETWEEN 6.02e23 AND f6 +f1 REGEXP "\t" +f4 IN () +(f3 BETWEEN "\n" AND "hello world") AND ((((f6 != f5) AND (f6 > 0.0000000001)) AND ((f4 <= f1) AND (f2 < .5))) OR (f1 == "%wild%")) +f6 BETWEEN .5 AND '' +f4 IN (0, 'O''Reilly', 10, -1e-10, "\n") +AND f3 = -.5 +f3 IN (,) +(f6 IN (f4, 999999999999, f5, 'test', "\n")) AND (f2 IN (0.0000000001, 10)) +f2 = NULL +f4 BETWEEN -1e-10 +AND f5 = 'O''Reilly' +((f3 == 6.02e23) OR (f6 ILIKE "abc")) AND (f4 IN (10, .5)) +(f6 = .5 +(f6 BETWEEN "\n" AND 3.14) OR ((f6 IN ('', 0, .5, .5)) AND (f4 <> 1)) +hasAll(f4, -2.7) +() +(f5 NOT CONTAINS "xyz%") AND (f6 BETWEEN .5 AND 1e10) +f6 === f2 +f1 IN (.5) +f5 IN (f1, 0) +f5 < f2 +f6 NOT REGEXP '' +f3 IN (,) +f2 NOT +f4 BETWEEN f3 AND 3.14 +f6 === 999999999999 +f3 BETWEEN f1 AND "\n" +has(f2, "hello world") +f6 <= -10 +f3 > "hello world" +f3 BETWEEN 0 AND -2.7 +f4 BETWEEN -2.7 AND .5 +(has(f4, ' ')) OR (has(f3, 10)) +f2 NOT REGEXP "\t" +AND f5 = f4 +(f6 > "abc") OR ((f4 BETWEEN ' ' AND f3) OR (f4 IN (0.0000000001, 'O''Reilly', 3.14, 'O''Reilly', TRUE))) +() +f1 = +((((f2 = f2) AND (f6 <> 1e10)) AND (f2 IN (' ', 10, f1, f5, "\n"))) AND (f1 IN ("abc", .5, 'test'))) OR (f3 REGEXP 'test') +(f2 REGEXP "\t") AND (f5 NOT CONTAINS "abc") +hasAll(f4, 'O''Reilly') +f3 != "\t" +f4 >= -1e-10 +hasToken(f2, .5) +f1 = '') +f5 = +(f2 < -1) AND (f4 NOT REGEXP "abc") +f4 BETWEEN "hello world" AND .5 +hasAny(f4, "a_b%") +f4 OR OR 1 +(f6 > f1) AND (f4 REGEXP 'O''Reilly') +f2 ILIKE '' +f1 IN ("abc", f1, -1e-10) +f6 != -1e-10 +AND f5 = 0.0000000001 +has(f2, 10) +hasAny(f3, 0.0000000001) +f1 CONTAINS "\t" +f3 > "hello world" +f6 BETWEEN 1e10 +f3 NOT LIKE "%wild%" +f3 NOT REGEXP 'O''Reilly' +((f3 BETWEEN '' AND '') OR (f6 BETWEEN "hello world" AND 999999999999)) OR (f1 != .5) +(f6 BETWEEN f2 AND -2.7) AND (hasAll(f5, 'O''Reilly')) +f5 BETWEEN 6.02e23 AND -1 +f5 REGEXP '' +f3 === f1 +(f1 IN (10, -1e-10, "\t", f5, f6)) AND (f4 BETWEEN f6 AND "\n") +f4 REGEXP 'test' +f5 BETWEEN 999999999999 AND 0 +f5 === "xyz%" +hasAll(f2, -1) +f2 IN (10, "abc", 999999999999) +f6 > '' +(f6 IN (TRUE, 'test')) OR (f6 = ' ') +f2 IN (1, -2.7, "\n") +f3 IS NULL +(f2 IN (3.14, f5, 1e10, f3)) AND (f2 BETWEEN f5 AND 1e10) +f3 != 1e10 +f6 LIKE +f6 BETWEEN 0 AND -2.7 +f1 = -1 +f3 != 0 +f1 IN (,) +f2 BETWEEN 10 AND "hello world" +f1 = -2.7 +f5 <= 6.02e23 +f1 BETWEEN 0.0000000001 AND f1 +f6 IN (0.0000000001, "\n", .5) +f2 == 'test' +has(f3, '') +f1 IN (,) +f3 IN ("hello world") +(f6 = "a_b%" +f1 NOT LIKE "\n" +(f1 BETWEEN "a_b%" AND -.5) AND (f3 NOT REGEXP ' ') +(hasAll(f6, -.5)) AND (f4 NOT CONTAINS '') +hasAny(f5, "\n") +(hasAny(f5, 3.14)) AND (f1 IN (1, '')) +f1 BETWEEN 10 AND ' ' +f3 BETWEEN f3 AND 6.02e23 +(f5 = .5 +(f1 IN (-10)) OR (has(f2, 6.02e23)) +f2 == 0 +f2 ILIKE ' ' +((f4 NOT REGEXP ' ') AND (f1 IN (10, -1))) AND (has(f2, "\t")) +f6 IN () +f6 BETWEEN 999999999999 AND "\t" +f4 BETWEEN 1e10 AND -10 +f4 = 'unclosed +f1 > "%wild%" +f2 IN (-2.7, -10, 999999999999, 0, 'O''Reilly') +f6 BETWEEN -1 AND "abc" +has(f5, f4) +f1 IN (0, ' ', 'test') +NOT +f1 BETWEEN "\n" AND f4 +f3 != 6.02e23 +f6 == 0.0000000001 +f6 OR OR -.5 +f1 CONTAINS 'test' +has(f5, 6.02e23) +f1 LIKE "hello world" +f3 IN () +f3 IN (-10) +f3 NOT CONTAINS 'O''Reilly' +AND f4 = .5 +hasAll(f5, '') +f5 NOT CONTAINS 'O''Reilly' +f6 === "abc" +f2 <= f3 +f4 IN (f3, -2.7, 3.14, 1e10) +f4 NOT ILIKE "%wild%" +f3 IN (f5, 999999999999, f4, .5, "hello world") +hasToken(f2, 6.02e23) +f5 IN ("\n") +f4 IN (999999999999, 10, 999999999999, 1) +(f6 = -10 +(f5 NOT ILIKE "\t") OR (f1 BETWEEN 3.14 AND f1) +f2 IN (' ', TRUE) +hasAny(f6, f1) +(f1 NOT ILIKE "xyz%") AND (f6 IN (0, 6.02e23, f3)) +f4 IN [1 2 3] +f6 BETWEEN -1e-10 AND TRUE +f5 BETWEEN "\n" AND 999999999999 +f6 BETWEEN 0 AND '' +f1 OR OR f3 +hasAll(f4, .5) +f4 IN (,) +f1 == 3.14 +has(f4, f5) +has(f5, f2) +f6 < TRUE +f5 CONTAINS "xyz%" +f1 NOT REGEXP ' ' +((f3 <> f6) OR (f6 IN ('O''Reilly', TRUE, "abc"))) AND (f1 LIKE "abc") +f5 IN ("a_b%", "%wild%", -3.5e-2, f1, "abc") +f2 CONTAINS "abc" +hasAny(f5) +f5 BETWEEN .5 AND "\t" +f6 = +has(f2, 3.14) +f6 != f3 +f2 IN (0.0000000001, 'O''Reilly') +f6 BETWEEN "\n" AND "xyz%" +f1 IN (1, -2.7) +has(f1, 6.02e23) +f1 BETWEEN -10 AND f5 +f2 = 'unclosed +f3 NOT CONTAINS "hello world" +f2 = .5 +NOT +f2 BETWEEN '' AND .5 +f1 = 'unclosed +f3 === f5 +(f1 REGEXP "hello world") AND ((f4 CONTAINS "\n") OR (f5 IN ("\n"))) +hasAll(f3, -10) +f5 < TRUE +(f4 IN (.5)) AND (f4 IN (' ', "abc")) +(f2 IN (.5, 0, "a_b%")) AND ((f4 BETWEEN "a_b%" AND 0.0000000001) AND (f3 NOT ILIKE '')) +has(f3, TRUE) +f3 NOT REGEXP 'test' +f4 LIKE 'O''Reilly' +f4 != "a_b%" +f5 BETWEEN 1 AND 'test' +hasToken(f1, 'test') +f1 LIKE "a_b%" +f4 == TRUE +f3 IN ("\t", 3.14, "a_b%") +f4 < f3 +(f5 IN (1)) AND (f1 != -2.7) +f6 NOT +f1 === 'O''Reilly' +NOT +hasToken(f4, "hello world") +(f1 BETWEEN "xyz%" AND 'test') AND (f5 IN (999999999999, -10)) +f1 NOT ILIKE "abc" +f4 NOT ILIKE ' ' +f5 BETWEEN f6 AND "a_b%" +f3 IS NULL +f5 IN [1 2 3] +f4 BETWEEN -2.7 AND .5 +f2 REGEXP "abc" +hasAll(f4, "xyz%") +hasAll(f1, f1) +f5 CONTAINS '' +f3 NOT REGEXP "xyz%" +f6 CONTAINS 'test' +f2 REGEXP '' +has(f4, 10) +f2 == 10 +(f5 = "\t" +(f4 IN (-3.5e-2, 999999999999)) OR (((f6 IN (-2.7)) AND (hasToken(f6, 'O''Reilly'))) AND (f3 BETWEEN -10 AND -3.5e-2)) +f2 LIKE "\t" +f1 IN ("xyz%", -10) +f5 > f4 +f2 NOT CONTAINS "xyz%" +f5 < 0.0000000001 +f2 BETWEEN -2.7 AND -2.7 +(f4 NOT REGEXP '') OR ((f3 IN (0, ' ', 'O''Reilly', "%wild%")) OR (f3 = 6.02e23)) +f2 IN () +f6 NOT LIKE 'O''Reilly' +f6 IN (,) +hasAll(f5, .5) +f5 NOT REGEXP 'O''Reilly' +f4 != f3 +hasAny(f3, 'O''Reilly') +f3 = f3 +f5 IN (10) +f4 > 0 +f3 BETWEEN 'test' AND f6 +has(f2, "hello world") +(f5 NOT LIKE 'O''Reilly') AND (f4 > "abc") +f6 IN [1 2 3] +f5 IN (-1e-10, "xyz%") +hasAll(f2, 'O''Reilly') +hasToken(f6, 1e10) +f1 ILIKE '' +f5 IN (f3, ' ', f1) +f4 LIKE '' +f1 ILIKE "\n" +f2 = +has(f4, 3.14) +(f5 IN ('test')) OR ((f1 = 999999999999) AND (hasToken(f6, -.5))) +(f3 BETWEEN 'O''Reilly' AND 0) OR (hasAny(f6, .5)) +f5 BETWEEN .5 AND -2.7 +f4 = f6 +f1 < "\t" +f2 BETWEEN "xyz%" AND f2 +NOT +hasToken(f2, f4) +(hasAll(f4, "hello world")) OR (f6 NOT LIKE ' ') +f2 LIKE 'O''Reilly' +has(f4, 'O''Reilly') +(has(f4, ' ')) AND (f6 NOT CONTAINS 'O''Reilly') +f3 BETWEEN f6 AND f6 +(f5 NOT LIKE "hello world") AND (f6 IN ("%wild%", f3, -.5, "hello world", 'O''Reilly')) +f1 IN [1 2 3] +f3 = NULL +f4 BETWEEN AND "a_b%" +f1 == f2 +f5 = 'unclosed +f6 REGEXP 'test' +f4 BETWEEN -1e-10 AND 3.14 +(f2 = -.5) AND (f4 IN (-10, "%wild%", 1e10, f5, -1e-10)) +(f5 = f2 +() +f3 BETWEEN "xyz%" AND "\n" +f2 < -10 +f5 BETWEEN f6 AND 'O''Reilly' +f5 == f4 +AND f3 = -1 +((f4 BETWEEN -10 AND -1) OR (f4 == f4)) AND (f6 IN (-2.7)) +hasAll(f2, f1) +f1 BETWEEN 10 AND 1 +f2 = f1) +f4 == '' +f4 IN ("abc") +hasAny(f1, TRUE) +has(f5, 0) +f2 BETWEEN f2 +(f2 IN ("\t", f4, .5, -10, -10)) OR (hasToken(f2, ' ')) +f2 LIKE +f6 NOT CONTAINS 'O''Reilly' +f5 BETWEEN 0.0000000001 AND 'O''Reilly' +f1 BETWEEN '' AND 6.02e23 +f6 BETWEEN f2 AND f3 +f1 NOT +(f4 = 0.0000000001 +f5 LIKE "\t" +has(f3, "%wild%") +f3 IN (,) +f5 = +(f2 < "%wild%") OR (f5 < '') +f6 IN (0, .5, 'O''Reilly', "a_b%", "xyz%") +() +f5 == "hello world" +AND f5 = -10 +f2 BETWEEN 10 AND 0.0000000001 +f2 >= "a_b%" +(f6 < "xyz%") AND ((has(f6, "abc")) OR (f1 >= .5)) +f2 BETWEEN "a_b%" AND '' +f6 BETWEEN 6.02e23 +AND f6 = f4 +(f1 = -1 +f6 BETWEEN .5 +hasAny(f3, "%wild%") +f2 IN ("a_b%", .5, "abc", f2) +f3 NOT LIKE "a_b%" +f3 >= 0.0000000001 +f2 ILIKE '' +f6 ILIKE "\n" +f3 IN (.5, 3.14, TRUE) +f5 BETWEEN f5 AND -1e-10 +f4 BETWEEN AND 0 +() +f1 IN ("%wild%", -2.7, -1) +f4 IN (f2, f1, 'test', f4) +((hasAny(f1, 0)) OR ((f4 NOT REGEXP ' ') AND (hasAll(f6, -2.7)))) AND (f4 IN (10, f6, f1)) +f3 ILIKE "hello world" +(f4 BETWEEN "hello world" AND "a_b%") AND (f3 IN (.5, 'test')) +f2 ILIKE 'O''Reilly' +f3 = +f5 <> 0 +f3 <> 'test' +f5 = f2) +f2 < .5 +NOT +f6 > -1 +(f2 IN (-1, 0, "abc", -.5, "\n")) AND ((f4 IN (f5, 1e10)) OR (f2 IN (3.14, 999999999999))) +f4 <> 1e10 +(hasAny(f4, ' ')) OR (hasAny(f6, 'O''Reilly')) +f3 LIKE +f3 BETWEEN f1 +f6 IN (-.5, f4, f5, TRUE) +f3 < f6 +f1 NOT REGEXP 'O''Reilly' +f4 NOT CONTAINS "%wild%" +f3 >= 0.0000000001 +f3 >= TRUE +f5 BETWEEN 999999999999 AND .5 +() +f5 <> '' +f5 BETWEEN "\t" AND 0.0000000001 +f3 BETWEEN "%wild%" AND TRUE +(f1 = -2.7) OR (f1 ILIKE 'O''Reilly') +f6 BETWEEN "abc" AND -1 +f4 <= -.5 +f3 IN [1 2 3] +f2 LIKE +(f4 REGEXP "%wild%") AND (f4 BETWEEN "hello world" AND f4) +(hasToken(f2, 1)) OR ((f5 IN (f2)) AND (hasToken(f1, 0.0000000001))) +f5 NOT LIKE "\t" +f6 BETWEEN 3.14 AND -2.7 +f4 BETWEEN "abc" AND 3.14 +hasAny(f4, 10) +f3 NOT +hasToken(f4, "a_b%") +AND f2 = f3 +f2 === -1e-10 +hasAny(f4, TRUE) +f1 IN ('test', "hello world", "a_b%", '') +f2 BETWEEN 'test' AND 999999999999 +NOT +f1 NOT REGEXP "%wild%" +f1 LIKE 'O''Reilly' +f6 BETWEEN -2.7 AND f3 +f5 >= -3.5e-2 +(f3 BETWEEN 0 AND -1e-10) OR (f4 IN ('O''Reilly', '', "\t", "xyz%", -1e-10)) +f1 BETWEEN 999999999999 AND "hello world" +hasAny(f1, "\n") +hasAll(f5, f4) +f2 <> 3.14 +AND f5 = 6.02e23 +f4 NOT CONTAINS "a_b%" +f2 <= "\n" +AND f6 = ' ' +f5 OR OR ' ' +f6 = 'unclosed +has(f2, 'O''Reilly') +f3 IN (0, "hello world", -1, "hello world", f4) +(f3 IN (1)) OR (hasToken(f6, f5)) +(has(f1, '')) AND (hasAny(f6, 1e10)) +() +f6 BETWEEN 1 AND "%wild%" +NOT +f1 IN (f2, -1) +f6 BETWEEN f6 AND TRUE +(f3 IN (' ', .5, "%wild%", f3, 3.14)) OR (f1 == 1e10) +f3 BETWEEN 1 AND -3.5e-2 +f2 BETWEEN AND '' +f2 IN (-.5, "a_b%", TRUE) +f3 BETWEEN -1e-10 AND 1e10 +f3 === 3.14 +hasAny(f4, 999999999999) +f4 = 'unclosed +f3 LIKE "xyz%" +(f2 NOT CONTAINS "\n") OR (f5 IN (-1, "a_b%", -2.7, ' ')) +f1 IN (,) +has(f2, f2) +(f4 = "%wild%" +f5 BETWEEN -2.7 AND TRUE +f5 IN (1, -3.5e-2, f6, -3.5e-2) +f5 OR OR 3.14 +f4 BETWEEN 0 AND 6.02e23 +f3 BETWEEN 'O''Reilly' AND 3.14 +f4 BETWEEN 3.14 AND "abc" +has(f3, f4) +f4 <= 999999999999 +(f6 = "a_b%" +hasAll(f3) +f5 IN ("\t") +f6 IN () +(f2 REGEXP 'O''Reilly') OR ((f3 <= 1e10) AND (f6 NOT LIKE ' ')) +f3 IS NULL +(f4 LIKE 'O''Reilly') OR (f6 IN (f2)) +f6 NOT REGEXP '' +f1 = 'unclosed +hasToken(f6, f1) +f1 IN (f5) +f5 > "xyz%" +(f5 < 1e10) OR (f4 IN (f2)) +(hasAll(f1, -1)) OR (f2 BETWEEN 3.14 AND -2.7) +f6 BETWEEN f5 AND f1 +AND f3 = "abc" +(f4 == "%wild%") OR (f4 LIKE "hello world") +f3 BETWEEN AND 3.14 +f2 CONTAINS 'O''Reilly' +f3 BETWEEN f6 AND -1 +f6 BETWEEN 'test' AND "hello world" +f1 BETWEEN TRUE AND -.5 +f5 NOT REGEXP "xyz%" +f6 > 0.0000000001 +f6 IN (f3, TRUE, "%wild%", "\n", f4) +f1 = 'unclosed +f6 BETWEEN "\n" AND "xyz%" +(f6 >= ' ') AND (f6 BETWEEN '' AND 'O''Reilly') +f4 IN (1, 6.02e23, -3.5e-2, ' ') +f1 REGEXP '' +f4 BETWEEN -.5 AND "\n" +f1 <= 999999999999 +f5 < 1e10 +f6 LIKE +((hasToken(f5, 0.0000000001)) OR (f4 REGEXP ' ')) AND (f4 BETWEEN 'test' AND 0) +f4 IN (-2.7, "\n") +f4 CONTAINS 'test' +hasAny(f2, f6) +f5 = +(hasAny(f5, -3.5e-2)) OR (f6 IN (1e10, f4)) +has(f2) +f2 ILIKE ' ' +f5 > -3.5e-2 +(f6 = TRUE) OR (f1 CONTAINS 'O''Reilly') +f5 OR OR 'test' +hasToken(f6, f1) +hasAny(f1) +f2 BETWEEN f3 +f3 NOT CONTAINS 'O''Reilly' +hasAny(f1, 6.02e23) +f2 > '' +f6 IN ('O''Reilly', f4) +(f6 BETWEEN -.5 AND 6.02e23) OR (f3 NOT ILIKE "abc") +f5 === 0 +f6 === "a_b%" +f1 NOT LIKE "a_b%" +f2 BETWEEN 6.02e23 AND "hello world" +((f4 IN (0, 0.0000000001, -1e-10, '')) AND (f6 NOT LIKE "a_b%")) AND (f5 IN (3.14, 10, f2, "abc", "abc")) +((f2 BETWEEN f3 AND 1e10) AND (hasAll(f3, 3.14))) AND ((f5 BETWEEN 'O''Reilly' AND 10) AND (f2 BETWEEN f4 AND f3)) +f3 == -1e-10 +f1 NOT CONTAINS 'test' +hasToken(f2, 'O''Reilly') +has(f4, f2) +NOT +f6 IN ("xyz%", "abc", "%wild%") +hasAll(f5, f2) +f4 BETWEEN 0 AND "a_b%" +f5 IS NULL +(f2 BETWEEN -2.7 AND 0.0000000001) AND (f5 IN ("a_b%", f4, -2.7)) +hasAny(f2, "a_b%") +f2 ILIKE "\n" +f5 IN (TRUE, "hello world", 0.0000000001, "xyz%") +f3 IN ('O''Reilly', "%wild%") +(f3 CONTAINS "\n") OR (f1 CONTAINS "abc") +f3 OR OR 999999999999 +f4 NOT REGEXP '' +f2 BETWEEN f6 AND f2 +f1 = NULL +hasToken(f5, 10) +(f5 IN (f2, 'test', .5)) OR (f6 BETWEEN -10 AND "a_b%") +f6 BETWEEN -1e-10 +(hasAll(f5, 1)) AND (f6 > '') +f4 = 'unclosed +(f5 IN (-.5)) AND ((f4 NOT CONTAINS "abc") AND (has(f3, f1))) +() +hasToken(f1, -1e-10) +f6 IN [1 2 3] +hasToken(f6, 1) +f1 IN ("\n", 1, 0, 0.0000000001, "a_b%") +f2 IN ("a_b%", f3, -1e-10, ' ', 6.02e23) +f2 BETWEEN 'O''Reilly' AND 1e10 +has(f5, -.5) +f3 > 0.0000000001 +f3 BETWEEN -3.5e-2 AND f1 +f2 IS NULL +(f6 = -1e-10 +f1 != f6 +f6 = 'unclosed +f5 > f4 +f4 = 0 +f4 NOT LIKE "a_b%" +f1 IN ('O''Reilly', 0.0000000001, 3.14, "\n") +NOT +(f1 <> 'test') OR (hasAny(f4, TRUE)) +hasAny(f2, '') +f1 BETWEEN 'O''Reilly' +f3 = f4 +f4 BETWEEN -1 AND "\n" +f6 IN () +f4 CONTAINS '' +has(f6, 'O''Reilly') +f2 IN ('', -2.7, f4) +hasAny(f2, 0.0000000001) +f2 BETWEEN 3.14 AND -2.7 +f5 IN ("abc") +f6 BETWEEN 'O''Reilly' AND "%wild%" +f6 REGEXP "hello world" +f6 IN (-1, "\n") +f2 BETWEEN 0 AND 6.02e23 +f2 BETWEEN "\t" AND "a_b%" +f4 NOT REGEXP 'test' +hasToken(f4, 999999999999) +f5 IN (.5, "hello world", f6, "%wild%") +f3 BETWEEN "%wild%" AND f5 +f4 LIKE +f1 NOT LIKE '' +f3 BETWEEN 'O''Reilly' AND f2 +f5 != "\t" +() +f6 BETWEEN -3.5e-2 AND '' +((hasAny(f2, f1)) AND (f1 IN ('test', "\n", TRUE, f4))) OR (f2 IN (0, -10, "xyz%")) +(f6 BETWEEN -10 AND 10) OR (f2 BETWEEN f4 AND -3.5e-2) +hasToken(f2) +f3 BETWEEN 999999999999 +f5 < -2.7 +f5 BETWEEN .5 +hasToken(f1) +f6 > "abc" +NOT +(f1 IN (-2.7)) OR (f5 BETWEEN f5 AND 0.0000000001) +(f6 >= "\n") OR (f5 == -2.7) +f2 > TRUE +f6 = 'unclosed +f6 > -2.7 +f1 NOT CONTAINS 'O''Reilly' +(f2 IN (f2, 10, "a_b%")) OR ((f2 IN ("a_b%", "\t")) OR (f4 BETWEEN 3.14 AND "%wild%")) +hasAny(f6, "a_b%") +f1 = NULL +f3 ILIKE "hello world" +f2 < f6 +hasAny(f3, "\t") +f2 == 3.14 +(f2 IN (' ', "\t", -.5)) OR (f6 < 3.14) +f4 BETWEEN -1 AND "abc" +(f4 <> 0.0000000001) AND (f4 BETWEEN 999999999999 AND 6.02e23) +f1 BETWEEN f4 AND f2 +hasAny(f5, TRUE) +hasToken(f5, "\n") +(f4 IN ('', "abc", "a_b%")) OR (f6 != "a_b%") +() +f2 === 10 +f5 LIKE "a_b%" +(f6 > -10) OR (has(f3, f5)) +f4 BETWEEN 999999999999 AND f2 +(f3 IN (f6, TRUE)) AND (hasAll(f6, 0)) +f1 === -10 +(f1 = "\n" +f3 BETWEEN 1e10 AND -2.7 +f6 BETWEEN f1 AND "xyz%" +f3 BETWEEN "\t" AND -1e-10 +(f3 >= -2.7) OR (f5 = 10) +f2 = 0 +f3 LIKE "\n" +f2 <> f5 +f3 BETWEEN -2.7 AND 6.02e23 +hasToken(f2, 1e10) +f1 LIKE '' +f1 = 'unclosed +hasToken(f3, "\n") +(f1 = f5 +f4 == 3.14 +f5 IN ("\t", -1e-10, 'test', "a_b%", -.5) +f3 BETWEEN f1 AND "\t" +f4 = f5 +hasToken(f4, 'O''Reilly') +f6 IN (-1) +f3 != "a_b%" +has(f1, 'O''Reilly') +f5 IN (f1) +hasAll(f2, -1) +f1 IN [1 2 3] +f1 <= 999999999999 +(f1 = 0.0000000001 +f5 BETWEEN "abc" AND "xyz%" +f4 NOT +f2 IN () +f2 >= -10 +f2 IN ("\t", 'test', 'test') +has(f3, .5) +f1 BETWEEN f5 AND -1e-10 +f4 >= -2.7 +f4 != 3.14 +f1 = 'unclosed +f6 IS NULL +(f4 ILIKE 'O''Reilly') AND (f2 BETWEEN -1e-10 AND "%wild%") +f1 NOT CONTAINS "xyz%" +NOT +hasAny(f6, "a_b%") +f1 BETWEEN 0.0000000001 AND 0 +hasToken(f1, .5) +hasToken(f6) +hasAll(f3, f3) +f4 BETWEEN '' AND 0.0000000001 +(f4 BETWEEN "xyz%" AND -3.5e-2) OR ((f3 ILIKE "abc") OR (f6 NOT ILIKE "%wild%")) +(f4 BETWEEN TRUE AND TRUE) OR ((f4 NOT CONTAINS "a_b%") OR (f6 IN (-.5, 999999999999, "abc", 999999999999))) +hasAll(f2, f3) +f3 REGEXP 'test' +f1 NOT CONTAINS 'O''Reilly' +f5 === -1e-10 +f1 = 6.02e23) +f6 BETWEEN -.5 AND f5 +has(f5, -2.7) +AND f1 = 0 +f3 IN (,) +hasAny(f2, -3.5e-2) +hasToken(f5, -3.5e-2) +(((f6 != f2) AND (f2 <= f4)) OR (f2 LIKE "\n")) OR (f5 ILIKE ' ') +(f4 = -.5 +f1 === 'test' +f2 IN ("hello world", 1, f5, "a_b%", -3.5e-2) +f6 BETWEEN 'O''Reilly' AND "xyz%" +f3 LIKE 'O''Reilly' +((f5 IN (-.5)) AND ((f3 <= -1e-10) OR (f4 LIKE "\t"))) OR ((f3 IN (f1, TRUE, "%wild%", -.5, ' ')) OR (hasToken(f2, 'test'))) +f6 NOT LIKE "\t" +f5 IN () +f1 BETWEEN -10 AND 0 +(hasAny(f1, f3)) OR ((has(f1, -.5)) AND ((f1 BETWEEN 3.14 AND 0) OR ((f2 = 3.14) OR (f3 <> f4)))) +f6 = +f3 BETWEEN 6.02e23 AND -1e-10 +(f2 BETWEEN "a_b%" AND 0.0000000001) AND (f6 REGEXP "\n") +(f4 BETWEEN "xyz%" AND ' ') AND (hasAll(f1, 'test')) +f1 = +f6 = NULL +f4 IN (-1e-10, 0.0000000001, -3.5e-2) +f4 IN (f3, "a_b%", "\t", f3) +(f4 = -.5 +f6 = 'unclosed +f4 ILIKE 'O''Reilly' +f4 <= -3.5e-2 +hasAny(f3, -1e-10) +(f4 ILIKE ' ') AND (f1 IN ("a_b%")) +hasAny(f3, "abc") +f2 CONTAINS ' ' +f6 IN ("%wild%", 'O''Reilly') +f5 NOT LIKE "\n" +f3 IN (-.5, '', f4) +f3 CONTAINS 'test' +f6 IN ("a_b%", f4) +f5 = NULL +f3 == "\n" +f5 = 'unclosed +f3 OR OR 0.0000000001 +f4 IN (,) +(hasAll(f5, "abc")) OR (f6 LIKE 'O''Reilly') +f3 BETWEEN 1 AND 1 +f1 NOT REGEXP ' ' +f2 NOT +hasAll(f4, -3.5e-2) +f1 IN (10) +(f5 = f1 +hasAll(f1, "a_b%") +has(f1, -.5) +f2 != -2.7 +(f3 > "%wild%") AND (f3 NOT LIKE ' ') +f5 <= "\t" +hasAny(f1, f2) +f2 IN (10, 1) +(f1 LIKE 'test') AND (f2 BETWEEN f6 AND "a_b%") +(f1 REGEXP ' ') AND (((has(f1, -1e-10)) AND (f2 BETWEEN f5 AND 0)) OR ((hasAny(f3, "\t")) AND (f5 BETWEEN 0 AND 'test'))) +f5 IN (-3.5e-2, -1e-10, "xyz%", "%wild%") +f4 IN ('test') +f5 CONTAINS 'test' +f4 == 999999999999 +hasAll(f6, 999999999999) +f3 IN (,) +f3 BETWEEN 999999999999 AND f5 +f4 IN () +f1 = 0 +f1 = 'unclosed +f6 IN (,) +f2 NOT LIKE "\t" +(f2 LIKE "xyz%") AND ((f5 REGEXP 'O''Reilly') AND ((f1 ILIKE 'test') AND (f6 CONTAINS "abc"))) +f4 CONTAINS "hello world" +f5 NOT +f2 IN (,) +hasToken(f3, f4) +f6 IN (6.02e23, "a_b%", ' ') +hasAll(f4, f3) +f3 BETWEEN f1 +() +(hasAny(f2, "\t")) AND (f1 LIKE '') +f3 != 0.0000000001 +f4 IN ("a_b%") +f5 IN ("\t", 0.0000000001, ' ', TRUE) +f2 >= 6.02e23 +(f2 = 'O''Reilly' +f5 BETWEEN -2.7 AND -1e-10 +f3 == 999999999999 +(f3 >= .5) OR (f6 IN (0, 'O''Reilly', "%wild%", "\n")) +f4 IN (f4, -1, f5, -1, f5) +f3 >= "\n" +f1 = NULL +f1 <> -2.7 +(f5 BETWEEN f4 AND -1) OR (f5 IN (999999999999, TRUE, -2.7, 0)) +has(f6, f6) +hasToken(f2, "hello world") +f5 LIKE 'O''Reilly' +(f2 NOT CONTAINS 'O''Reilly') AND (f3 > f1) +f1 BETWEEN -1e-10 AND f4 +f4 != "\n" +f2 BETWEEN "%wild%" AND f5 +f6 NOT LIKE "xyz%" +f6 BETWEEN 1 AND f2 +(f6 > -2.7) OR (f1 REGEXP "%wild%") +f1 NOT LIKE "xyz%" +(f4 NOT ILIKE "hello world") AND (f2 == 1e10) +f6 NOT REGEXP '' +f6 != TRUE +f2 <> 6.02e23 +(f1 NOT REGEXP "abc") AND (f5 < 0) +f3 BETWEEN "abc" AND f4 +AND f2 = f4 +has(f2, f4) +f5 != "abc" +f6 NOT REGEXP 'test' +(f4 = -1e-10 +f6 BETWEEN -.5 AND f4 +hasAny(f4, "abc") +f4 IN (f1) +f1 NOT LIKE "hello world" +f1 IN (,) +f4 BETWEEN f4 +has(f6, "xyz%") +f3 IN (f6, 0.0000000001, "a_b%", "a_b%", "abc") +f3 IN ("%wild%", TRUE) +hasAny(f4, -.5) +f6 IN (,) +f5 BETWEEN "%wild%" AND 1e10 +f4 NOT +f1 IN ("\t", 0, -10, -1e-10) +f3 = NULL +f4 CONTAINS "a_b%" +f1 BETWEEN '' AND 10 +f3 == "hello world" +f6 <= 999999999999 +(f1 == "%wild%") OR (f4 IN (0.0000000001, 0.0000000001)) +hasAll(f3, "abc") +hasAll(f5, 0.0000000001) +f2 LIKE "hello world" +(f4 IN (-2.7)) AND (f2 NOT ILIKE ' ') +f2 BETWEEN "hello world" AND -2.7 +(f5 CONTAINS ' ') AND (f1 BETWEEN 3.14 AND TRUE) +f2 CONTAINS "\n" +NOT +f5 NOT ILIKE "a_b%" +f5 <= -3.5e-2 +(f2 = 'test' +f1 NOT ILIKE "\t" +f6 LIKE "%wild%" +f4 IN (f5) +f4 NOT LIKE "%wild%" +f3 BETWEEN '' AND "xyz%" +f2 BETWEEN -1e-10 AND -1 +NOT +f4 IN (,) +f2 == 10 +(f5 IN (f1, -3.5e-2, "abc", 1)) AND (has(f6, -10)) +f3 = +f3 BETWEEN f6 AND 1 +f4 != -3.5e-2 +f2 BETWEEN "abc" AND '' +f4 IN (f3, 1e10) +(f6 BETWEEN -1 AND -10) OR (f1 BETWEEN 'test' AND f3) +f6 CONTAINS '' +f5 NOT CONTAINS "\t" +f3 = 'unclosed +f3 IN (TRUE) +f3 IN ("a_b%") +f2 <= 6.02e23 +hasAny(f6, 'O''Reilly') +f1 BETWEEN 0.0000000001 AND f2 +f4 NOT CONTAINS "a_b%" +f4 BETWEEN AND -.5 +f6 CONTAINS "xyz%" +f5 IN (f4, -1, f4, -10, ' ') +f5 IN (0, -2.7) +(f1 IN (f6)) AND ((f1 IN ("\t", "%wild%", f3, ' ')) OR (f3 IN (.5))) +f4 NOT +f1 BETWEEN "\n" AND "hello world" +f1 < 0 +f2 IN () +f3 >= -10 +f6 BETWEEN f5 AND .5 +f3 NOT REGEXP ' ' +f3 = NULL +hasToken(f3, "\t") +hasAll(f3, -2.7) +f5 = '') +f5 NOT REGEXP 'O''Reilly' +f5 BETWEEN f4 AND -10 +f1 OR OR f4 +f4 BETWEEN f5 AND "\t" +f4 NOT ILIKE 'O''Reilly' +f3 BETWEEN -3.5e-2 AND "a_b%" +f1 == 3.14 +f1 === f5 +f3 IN (3.14, -1, -3.5e-2) +f3 = NULL +has(f3, 'test') +f5 REGEXP "hello world" +(f4 <> 'test') AND (f3 ILIKE ' ') +f3 = 'unclosed +f6 BETWEEN 6.02e23 AND -1 +hasAny(f1, -2.7) +f2 = f1) +(((f6 IN (f4, -1)) OR (f4 BETWEEN -1e-10 AND ' ')) AND (f1 < "\n")) AND (f4 CONTAINS "a_b%") +f6 IN ("a_b%", 1e10, 0, -3.5e-2) +f4 = NULL +f2 = +hasAny(f2, 6.02e23) +((f5 IN (-1e-10, f1, -1, f3)) OR (has(f4, 'test'))) OR (f4 == f4) +hasAny(f4) +hasToken(f3, "xyz%") +f1 BETWEEN 0.0000000001 AND f2 +f4 = -10 +f3 LIKE +f4 = NULL +f6 BETWEEN "xyz%" AND -3.5e-2 +(f2 CONTAINS 'test') AND (f1 IN ('O''Reilly', -10, -10)) +f1 LIKE +has(f4, f4) +f1 IN (3.14, ' ', 1e10) +f5 BETWEEN "xyz%" AND 1 +f5 OR OR -3.5e-2 +f4 NOT LIKE '' +(f4 = f5 +f2 IN (-1) +f5 > 1 +NOT +f5 != f4 +hasAll(f6, .5) +f6 > "abc" +f3 LIKE "xyz%" +f6 IN ("xyz%", "\n") +f3 = 'O''Reilly') +f3 IN (3.14, -.5) +f5 === .5 +hasAny(f1, 10) +has(f5, 'O''Reilly') +f6 NOT +f1 BETWEEN -2.7 AND 0 +f1 NOT CONTAINS "a_b%" +f3 != "abc" +f2 BETWEEN -2.7 AND 1 +(f2 BETWEEN f6 AND "abc") OR (f6 < -1e-10) +f1 BETWEEN f1 AND 999999999999 +f2 REGEXP "a_b%" +hasAll(f4, "hello world") +f6 <= "xyz%" +f1 NOT LIKE "%wild%" +((has(f2, "abc")) AND ((f2 BETWEEN -1 AND -10) OR (f2 BETWEEN "hello world" AND .5))) AND ((((f4 == "%wild%") AND (f4 != 6.02e23)) AND (f3 IN ("\n", "hello world", f2, -1e-10))) AND (f4 BETWEEN ' ' AND -1e-10)) +f6 = 0) +f1 >= 'test' +f3 != 'test' +f1 NOT CONTAINS '' +f3 REGEXP "\t" +f6 BETWEEN "hello world" AND f4 +(f3 = "abc" +hasAny(f5, .5) +f5 IN (-2.7, "hello world", 'O''Reilly') +f1 == "abc" +f3 IN [1 2 3] +f2 ILIKE "xyz%" +(f6 IN (-1e-10, "\t", ' ')) AND (f4 NOT ILIKE 'test') +f3 IN ("abc", "%wild%", f5, 0.0000000001) +has(f1, -2.7) +f2 BETWEEN TRUE AND -1 +f5 NOT LIKE "xyz%" +hasAny(f4, 0.0000000001) +f3 BETWEEN -2.7 AND -1 +has(f4, "hello world") +f2 NOT LIKE "a_b%" +f5 = +f4 = +f3 IN (-3.5e-2, 1, -2.7, 1) +f2 BETWEEN -3.5e-2 AND 0.0000000001 +(f2 BETWEEN -1e-10 AND -.5) OR (f1 NOT CONTAINS 'test') +f1 <> '' +f6 IN (6.02e23, -10, -3.5e-2, f4) +f3 BETWEEN -1 AND 1e10 +f1 NOT LIKE "hello world" +f4 BETWEEN -2.7 AND f1 +f3 BETWEEN "%wild%" AND f2 +hasAny(f5, f2) +hasAll(f4, "a_b%") +f6 BETWEEN 1 AND -1e-10 +f5 NOT CONTAINS "xyz%" +NOT +f2 BETWEEN "abc" AND 10 +has(f5, "abc") +f3 == "xyz%" +hasAny(f2, -1) +f2 BETWEEN 0.0000000001 AND 3.14 +has(f4, f1) +hasAll(f1, f3) +f3 IN (-1) +f1 = NULL +f1 LIKE +f5 = NULL +f2 != 'test' +f3 <= f4 +f4 LIKE "abc" +f6 < "abc" +(f3 < 6.02e23) OR (f5 NOT LIKE 'test') +f5 IN ("a_b%", '') +f3 IN ("\n", -1) +f6 >= 10 +f4 BETWEEN f3 AND ' ' +f1 NOT +(f1 = "abc" +f5 BETWEEN -10 AND "\t" +f6 BETWEEN -10 AND 6.02e23 +hasAll(f6, -3.5e-2) +((f4 IN (-3.5e-2, 1, "abc")) OR ((f6 IN (f1, 999999999999, -2.7, 'O''Reilly', "xyz%")) AND (f1 CONTAINS 'O''Reilly'))) OR (f3 BETWEEN '' AND 'test') +f1 = +f4 REGEXP "%wild%" +NOT +f3 IN (f3, 10) +f1 IN (10, 'test', "abc") +f2 BETWEEN ' ' AND -1e-10 +f3 IS NULL +(hasAny(f1, "abc")) AND ((f4 BETWEEN 999999999999 AND -.5) OR ((f1 BETWEEN TRUE AND -1) AND (f5 >= "a_b%"))) +f1 = +f4 IN (-.5) +f5 LIKE +f4 IN (,) +f5 < -2.7 +f2 BETWEEN "\t" AND f4 +f5 != TRUE +(f5 BETWEEN f6 AND 6.02e23) AND (f4 BETWEEN "hello world" AND "%wild%") +f1 < -1 +(f5 BETWEEN TRUE AND -1) OR ((f2 IN ("\t", f1, f1, -1)) AND (f6 != f1)) +f1 = +(f3 <> -2.7) AND (f3 BETWEEN "%wild%" AND ' ') +f5 ILIKE "%wild%" +f6 NOT CONTAINS "%wild%" +() +hasAny(f4, "%wild%") +f6 ILIKE "%wild%" +f6 BETWEEN '' AND .5 +(f6 LIKE "\n") AND (has(f3, 'test')) +f1 BETWEEN f2 AND "xyz%" +f4 BETWEEN TRUE AND "\n" +AND f6 = f3 +f3 = f6 +f5 = 6.02e23) +hasToken(f1, 1) +f4 = 'unclosed +(hasAny(f5, "a_b%")) OR ((has(f2, 'test')) AND (f4 IN (6.02e23))) +f5 NOT +hasToken(f4, ' ') +f2 = f4 +(f1 = -1e-10 +f5 BETWEEN 'O''Reilly' AND f6 +f1 != f4 +(f1 = 10 +f1 <> "%wild%" +f1 BETWEEN "hello world" +f2 NOT ILIKE "\n" +f6 = NULL +hasToken(f3, -3.5e-2) +f2 CONTAINS "%wild%" +hasAll(f2, 1) +f6 >= f4 +f1 = 'unclosed +f1 BETWEEN -.5 AND "a_b%" +f2 BETWEEN ' ' AND "\t" +hasToken(f1, 0) +f6 BETWEEN -2.7 AND 3.14 +f4 == 0.0000000001 +f2 = 'unclosed +f6 < 'O''Reilly' +f6 != "a_b%" +f3 LIKE "xyz%" +f1 IN (f2, 999999999999) +f4 >= -2.7 +f4 = NULL +f5 IS NULL +f2 NOT ILIKE "xyz%" +f2 IN [1 2 3] +(f6 NOT LIKE ' ') OR ((f4 NOT ILIKE "%wild%") AND (f3 = "\n")) +f6 != 1e10 +f3 IN (3.14, "\t") +(f3 >= -2.7) OR (f5 ILIKE "xyz%") +f1 BETWEEN -.5 AND "a_b%" +f5 LIKE '' +(f4 BETWEEN 'test' AND 1) AND (f1 BETWEEN "%wild%" AND -1) +() +(f3 IN (f6, f3, -1, f2)) AND (f2 IN ("\t", f2)) +f5 BETWEEN 1e10 AND 1 +() +hasToken(f5, f6) +f2 BETWEEN "a_b%" AND -1e-10 +((f6 IN (0, "\t", 1e10, "abc", '')) AND ((f6 > "\n") OR ((f3 >= -1) AND (f6 == -3.5e-2)))) OR (hasAll(f2, '')) +f5 BETWEEN "\t" AND f3 +f2 BETWEEN ' ' AND 'test' +f6 BETWEEN -.5 AND "hello world" +f2 === "%wild%" +f6 IN (1e10, "%wild%", "\n", -3.5e-2) +f2 LIKE +f3 NOT REGEXP "abc" +has(f4) +f6 IN (.5, f6, -10, .5, f4) +f1 BETWEEN -1e-10 AND "%wild%" +f1 IN ('') +f1 IN ('test', f3, -2.7) +f2 IN (999999999999) +f5 IN (TRUE, f4) +hasAny(f1, f1) +f1 IN ("hello world") +NOT +hasToken(f1) +f5 BETWEEN "%wild%" +f5 = NULL +hasAll(f3, "%wild%") +f3 NOT REGEXP "\n" +hasAll(f2, .5) +f6 IN (,) +f5 BETWEEN 0.0000000001 AND -10 +f2 = NULL +f6 BETWEEN f1 AND 1e10 +f3 IN ("\n", "\n", f5) +(f4 NOT CONTAINS 'O''Reilly') OR (f6 BETWEEN 0 AND -.5) +hasAny(f6, '') +f5 <> 1e10 +f6 != -1e-10 +f2 IN () +f3 NOT CONTAINS "hello world" +hasAll(f4, 1) +f4 NOT LIKE "\t" +f3 BETWEEN f4 AND f3 +(hasAll(f6, f4)) AND (f2 LIKE "a_b%") +f5 >= "\n" +f5 === "\t" +(f4 BETWEEN 999999999999 AND 1e10) AND (f2 IN (6.02e23, 999999999999)) +f1 BETWEEN f1 AND "a_b%" +hasAll(f1, "abc") +f2 BETWEEN -3.5e-2 AND "a_b%" +(hasAny(f5, "a_b%")) OR (f4 IN (f5)) +f6 NOT CONTAINS 'test' +f3 NOT ILIKE "abc" +f3 IN (f5, .5, 1e10, f2, f2) +hasToken(f2, -10) +f2 IN (f3, f5, .5, 999999999999, f5) +f2 IN ("abc", -.5, 6.02e23, 1, -2.7) +f4 IN (-1e-10) +f3 >= f3 +hasToken(f5, ' ') +((f6 IN (' ', 1e10, "abc", "\t", 10)) AND (f1 BETWEEN "hello world" AND 'O''Reilly')) AND (f6 NOT LIKE "\n") +f5 = +NOT +has(f2, TRUE) +AND f1 = f5 +f1 BETWEEN f3 AND TRUE +(f1 == .5) AND (f4 NOT LIKE "hello world") +f1 = 'unclosed +hasToken(f2, "\n") +hasAny(f1, f3) +(hasAny(f1, 'test')) AND (f1 < 1) +(f6 = 6.02e23 +hasAll(f2, ' ') +f6 = 999999999999 +hasAll(f5, -.5) +f5 = 'unclosed +f5 IN ('', "\n", 'O''Reilly') +f2 BETWEEN AND 999999999999 +f5 IN (f4, 0.0000000001) +f1 IN (10, f2) +f4 <= 1 +f2 BETWEEN 'test' AND -2.7 +f2 IN (0.0000000001) +f1 IN (10, .5) +f4 IN (f2, '', f1, 'O''Reilly') +f4 IN (' ', '', 'test', f1, 1e10) +f3 BETWEEN AND f4 +f5 == -1 +(has(f2, f2)) OR (f1 IN (-3.5e-2, f1, -1, 0)) +(f4 IN (6.02e23)) AND ((f6 IN (f3)) AND (f5 BETWEEN -1 AND TRUE)) +f1 BETWEEN f3 AND 0.0000000001 +f3 > "hello world" +f4 BETWEEN 1e10 AND .5 +f1 <= 1e10 +hasAll(f6, f2) +f5 REGEXP ' ' +f5 = "%wild%" +(hasAny(f4, f2)) OR (f4 CONTAINS "hello world") +hasAny(f1, -.5) +f1 LIKE "abc" +f4 BETWEEN AND TRUE +f6 NOT CONTAINS "xyz%" +f1 IN () +f1 BETWEEN "xyz%" AND f6 +hasToken(f3, "\t") +(f5 = .5 +(f4 <= -2.7) AND (f4 BETWEEN 'test' AND 10) +hasAny(f3, 0.0000000001) +f3 NOT +f6 ILIKE "abc" +f5 >= -3.5e-2 +f1 ILIKE "\t" +f6 < -2.7 +f5 = +hasAll(f5, f6) +f5 <= "xyz%" +f2 BETWEEN .5 AND f2 +f3 = +f4 >= "a_b%" +f2 <> "\n" +f5 IN (3.14, 3.14, -3.5e-2, f6) +f6 NOT +f2 IN ("xyz%") +f1 = NULL +(hasAll(f2, "\n")) OR (f4 <= 1e10) +f5 <= 'test' +f4 IN (,) +f1 ILIKE '' +f2 ILIKE "a_b%" +f6 BETWEEN TRUE AND "hello world" +f1 BETWEEN f6 AND 0.0000000001 +f5 NOT ILIKE "\n" +f2 REGEXP "%wild%" +has(f4, f1) +f1 < -10 +f5 <> ' ' +hasAny(f6, 999999999999) +f2 <> f4 +f3 != "abc" +f3 IN (f6, -2.7, "\t", "%wild%", 1) +f3 BETWEEN "xyz%" +f6 BETWEEN 'test' AND "hello world" +hasToken(f4, -10) +((hasAll(f2, -1)) AND (f4 ILIKE "xyz%")) OR (f4 < TRUE) +f2 > 1e10 +(f6 = 'O''Reilly' +(f3 IN ("xyz%", "a_b%", 10, .5, .5)) AND (((f6 >= '') AND ((f6 = ' ') AND (f1 >= 3.14))) OR ((f2 <= 999999999999) OR (f2 IN (-.5, -2.7, f6, "xyz%")))) +hasToken(f1, f5) +f5 < -.5 +hasAny(f3, 10) +f4 REGEXP '' +f4 CONTAINS "abc" +f3 ILIKE "abc" +f3 IN (,) +(f2 = '' +f2 OR OR -2.7 +f1 <= -3.5e-2 +f1 IN (,) +f5 IN ("\n") +f2 < -2.7 +hasToken(f3, 10) +has(f1, f4) +f4 NOT CONTAINS '' +f2 > f2 +AND f1 = f2 +f1 BETWEEN 999999999999 AND "xyz%" +f3 NOT ILIKE "%wild%" +hasToken(f6, 10) +f6 IN ('', "xyz%") +f2 NOT +(f3 IN ('test', f3, "%wild%")) AND (f1 BETWEEN .5 AND -.5) +has(f2) +((f4 BETWEEN -3.5e-2 AND 1) AND (hasToken(f5, -3.5e-2))) AND (f3 LIKE "\t") +f5 <= "abc" +f3 NOT +f3 BETWEEN 0.0000000001 AND 'O''Reilly' +((hasToken(f1, f2)) OR (has(f3, f1))) OR (f3 BETWEEN "xyz%" AND "xyz%") +f3 = 'unclosed +has(f1, "a_b%") +hasAny(f1, f3) +f5 IN [1 2 3] +f1 REGEXP ' ' +f4 IN (-1, 3.14) +f5 === 1e10 +f5 BETWEEN -2.7 AND 6.02e23 +f6 IN [1 2 3] +f2 BETWEEN 6.02e23 AND .5 +f1 != 0.0000000001 +f1 == "\n" +f1 LIKE "abc" +((((f1 = 3.14) OR (f2 > "\n")) AND (f1 IN ('test', "a_b%"))) AND ((has(f5, f4)) OR (f5 ILIKE 'test'))) OR (f4 IN ("a_b%", -2.7, -1, 0, -1e-10)) +f4 <= 1e10 +f3 IN ("\n", f2, -10, "%wild%", 1) +(f1 = -10 +(f3 = "a_b%" +hasToken(f3, -1) +f3 <> f1 +f3 BETWEEN 6.02e23 AND "\n" +hasToken(f5, -3.5e-2) +f1 NOT ILIKE '' +has(f5, f4) +((hasAll(f1, 1)) OR (hasAny(f4, ''))) OR (f5 >= "\n") +hasAny(f2, 1) +() +f5 LIKE +f3 NOT LIKE "\t" +f3 CONTAINS "\n" +f1 != "hello world" +(f2 CONTAINS 'O''Reilly') AND (f2 BETWEEN f4 AND "%wild%") +f4 = "xyz%" +f3 IN (-2.7, ' ') +f4 IN ("a_b%") +has(f2, 6.02e23) +f4 IN (1) +f1 === 0.0000000001 +f3 BETWEEN f4 AND 1e10 +hasAll(f5, "hello world") +f4 != -3.5e-2 +(f1 BETWEEN -.5 AND TRUE) AND (f5 NOT REGEXP "abc") +f3 = 3.14 +f1 IN (1e10, f3, -2.7, 3.14, "a_b%") +f6 = '' +f1 ILIKE 'test' +f1 CONTAINS "\n" +f1 >= "xyz%" +f6 = f3) +hasToken(f2, ' ') +f3 IN (.5, TRUE, 3.14, f6) +f4 BETWEEN AND -1e-10 +hasToken(f6, 0.0000000001) +f4 < 999999999999 +(f6 = TRUE +() +hasAny(f3, "a_b%") +f4 IS NULL +(f6 = 3.14 +has(f2, -1) +f3 IN (-1) +(((f2 == 3.14) AND (f5 CONTAINS "xyz%")) AND (f3 BETWEEN 'test' AND f2)) OR (f6 != -3.5e-2) +f6 NOT REGEXP 'test' +f6 IN (1e10) +f1 = -3.5e-2) +f6 IN [1 2 3] +(f1 IN ('test', -1, f5, -2.7)) OR ((f4 BETWEEN 10 AND ' ') OR ((f6 BETWEEN -10 AND 10) AND (f3 NOT ILIKE 'O''Reilly'))) +f1 = 999999999999) +hasAll(f6, -10) +f6 IN (-2.7, 0.0000000001, "%wild%", 1e10, ' ') +f3 BETWEEN "%wild%" AND 6.02e23 +f1 LIKE "abc" +f5 LIKE "\t" +has(f4, -10) +(f3 LIKE 'test') OR (hasAny(f3, 999999999999)) +hasAll(f3, '') +f1 = +(f1 >= 999999999999) OR (f3 IN ("\t", 1e10)) +f5 REGEXP "hello world" +(((f2 BETWEEN 'O''Reilly' AND "%wild%") AND (f1 = -1)) OR ((f2 IN (TRUE)) OR (f1 BETWEEN 6.02e23 AND .5))) AND (f4 NOT CONTAINS 'test') +f5 IN () +(f3 ILIKE "%wild%") AND (f2 BETWEEN TRUE AND 1e10) +f6 IS NULL +() +f4 BETWEEN -3.5e-2 AND -2.7 +hasAny(f6, -10) +hasToken(f1, 0) +hasToken(f1, -2.7) +f5 === '' +f4 = 10 +f3 === 999999999999 +f3 === 10 +f5 BETWEEN 999999999999 AND 'test' +(f6 BETWEEN 6.02e23 AND 10) OR (f3 < f5) +AND f6 = -.5 +(f1 BETWEEN 999999999999 AND 10) OR ((f2 BETWEEN -.5 AND -.5) OR (f3 ILIKE "xyz%")) +(f2 == TRUE) OR (f4 < "\t") +(f6 IN (0.0000000001, f2, f3, f3, ' ')) OR (hasAny(f1, -.5)) +f6 BETWEEN AND 1 +(f2 = -10 +hasAny(f1, .5) +f5 = '' +f4 IN (f3, -1, "\n", 0.0000000001) +f1 BETWEEN f6 AND "\t" +f3 < "xyz%" +f4 <= "\t" +f6 === 0.0000000001 +f2 BETWEEN "\n" AND -.5 +f1 < 6.02e23 +f6 ILIKE "a_b%" +f5 LIKE +f2 NOT ILIKE 'O''Reilly' +f6 IN (,) +f2 BETWEEN AND '' +f2 OR OR 1 +f6 = 'unclosed +f4 BETWEEN '' AND f4 +AND f3 = "\n" +f3 IN (1, 0, "%wild%") +f5 OR OR "\t" +f3 NOT CONTAINS "hello world" +f1 BETWEEN f3 AND "hello world" +f6 IN (.5, "hello world", 1e10, "\n", .5) +f3 BETWEEN -3.5e-2 +f2 = +(f4 > -3.5e-2) AND (f1 > f2) +hasAll(f3, -3.5e-2) +((f5 NOT CONTAINS 'O''Reilly') OR (((f1 == f5) AND (f6 != 1)) OR (f6 IN (0.0000000001, f2)))) OR (f2 CONTAINS "%wild%") +f2 NOT ILIKE ' ' +f1 IN (' ') +f4 = NULL +f2 = .5) +f2 IN (.5, 999999999999, "\t", 3.14, f5) +f1 === -1e-10 +hasAll(f1) +f6 BETWEEN f2 AND 0.0000000001 +f5 BETWEEN 1e10 AND 0 +f6 NOT ILIKE "a_b%" +f4 BETWEEN f1 AND 3.14 +f4 <= 3.14 +f5 === f6 +f2 LIKE +hasAny(f2, 3.14) +f4 NOT +f2 BETWEEN 6.02e23 AND 6.02e23 +f6 === -2.7 +f4 IN ("\n") +hasAny(f3, '') +f2 NOT CONTAINS 'test' +(hasAll(f1, "\t")) OR (f1 IN (0, f5)) +f5 BETWEEN 'O''Reilly' AND .5 +f2 IN (-10) +f2 CONTAINS "\t" +f6 IN (0.0000000001, f4, '', "\n") +(f1 NOT REGEXP "a_b%") OR (hasAny(f1, -10)) +f6 REGEXP "xyz%" +(f4 IN (1, 3.14, "\n")) AND (hasToken(f3, f5)) +f1 BETWEEN f4 AND 0 +AND f1 = -3.5e-2 +f2 < -10 +f5 = 'unclosed +f4 NOT CONTAINS 'test' +hasAny(f2, f2) +(f3 NOT CONTAINS "\t") OR (f2 IN ('O''Reilly', -1e-10)) +f6 <= "%wild%" +f5 > 1e10 +f1 BETWEEN -3.5e-2 AND .5 +f1 IN (f5, "\n", "hello world") +() +f3 IN (f1, f5, "\t", 'O''Reilly', f2) +f1 IN (-1e-10, "\n", ' ', f1) +f3 > -1 +f3 BETWEEN "a_b%" AND 0.0000000001 +f5 <= "hello world" +hasToken(f2, f2) +((f5 BETWEEN "\n" AND 0.0000000001) OR (f1 BETWEEN '' AND f3)) AND (f3 BETWEEN ' ' AND .5) +f4 BETWEEN TRUE AND 999999999999 +f1 BETWEEN "a_b%" AND 'test' +f5 BETWEEN TRUE +has(f3) +NOT +f6 = NULL +f5 BETWEEN "hello world" AND -2.7 +f1 >= -.5 +f1 BETWEEN f4 AND -1 +f5 IN (-1e-10, 'test', TRUE) +f5 ILIKE "hello world" +f1 < "hello world" +f4 IN (6.02e23) +f5 BETWEEN 10 AND 1 +(f2 == 999999999999) OR (f4 IN ("\t", -1e-10, "%wild%")) +(f1 CONTAINS "xyz%") AND (f3 > 'O''Reilly') +(f4 NOT ILIKE "xyz%") AND (f5 BETWEEN '' AND 10) +f3 IN (,) +f3 BETWEEN TRUE AND 999999999999 +f3 <> f2 +f5 CONTAINS '' +f1 IS NULL +AND f1 = "xyz%" +f1 IN (-3.5e-2, "hello world") +f2 IN ("%wild%", -3.5e-2, -1e-10, -1e-10) +f5 BETWEEN 0.0000000001 AND 0 +f3 BETWEEN "\t" AND -2.7 +f2 = 0.0000000001 +f4 NOT CONTAINS "xyz%" +f6 <= f3 +f4 <= 6.02e23 +f5 BETWEEN AND ' ' +f2 BETWEEN "xyz%" AND f5 +f2 < -.5 +hasToken(f3, -10) +f5 OR OR -.5 +(f6 IN (f3, -2.7, f4, f5, "a_b%")) OR (f1 ILIKE 'O''Reilly') +f6 IN (0.0000000001, f2, 6.02e23, f4, 6.02e23) +f2 != "a_b%" +hasAll(f6, "\n") +hasAll(f3, f6) +f2 BETWEEN f5 AND "abc" +f5 NOT LIKE "hello world" +f1 = .5 +hasAny(f1, ' ') +hasToken(f5) +f1 IN ("xyz%", ' ', 1e10, "a_b%", .5) +f2 BETWEEN 6.02e23 AND f5 +f6 IN (,) +f4 != "\n" +(hasToken(f6, -1e-10)) AND ((f6 <> -1e-10) OR (f3 IN (1, 'O''Reilly', f6, f5))) +has(f3, "hello world") +f1 BETWEEN '' AND "\n" +(f6 = "xyz%" +(f5 BETWEEN "a_b%" AND TRUE) AND (f2 IN (f5, f4, "a_b%", -3.5e-2)) +f5 IN (1, TRUE) +f3 NOT LIKE "hello world" +f2 = "a_b%") +f5 BETWEEN "a_b%" AND "a_b%" +f6 IN (,) +f1 BETWEEN f1 AND "\n" +(has(f5, "%wild%")) AND (f2 BETWEEN -2.7 AND 0.0000000001) +f1 < 0.0000000001 +f1 BETWEEN 3.14 AND 1e10 +f1 = 6.02e23) +hasToken(f4, 'O''Reilly') +f1 = +f2 NOT CONTAINS ' ' +f2 NOT REGEXP '' +((f4 IN (.5, f1, -.5)) AND (f6 IN (.5, 6.02e23))) OR (f2 BETWEEN -10 AND -1e-10) +f6 IN (-1) +hasAny(f3, .5) +f5 NOT LIKE "xyz%" +hasAny(f4, TRUE) +hasAll(f5, -2.7) +hasAny(f6, .5) +f1 <> f1 +f6 != 0 +f2 IN ("%wild%", -1e-10) +f1 BETWEEN 0 AND "a_b%" +f2 IN ("abc", "abc", 0) +f5 === -3.5e-2 +f4 = NULL +(has(f3, "\t")) AND (f4 NOT LIKE 'test') +f1 IN (-10) +(f2 BETWEEN 0.0000000001 AND 6.02e23) AND ((hasAny(f6, ' ')) AND (f6 NOT LIKE '')) +((f4 NOT CONTAINS "%wild%") AND ((f6 NOT REGEXP "\n") AND (f6 == "hello world"))) AND (f5 != f3) +f1 NOT CONTAINS '' +f5 IN ("abc", 'O''Reilly', ' ', f5) +((f6 BETWEEN "\t" AND "\n") OR ((hasAny(f4, ' ')) AND (f3 BETWEEN f2 AND 999999999999))) OR (hasAny(f5, "%wild%")) +hasToken(f3, 0) +f6 = TRUE) +f1 IN ("\t", f5, 1, 0) +hasAll(f4, f5) +f6 IN ('test', "\t", "hello world", -10, f6) +hasAll(f5, .5) +f3 BETWEEN f3 AND "%wild%" +(has(f5, "\n")) OR (has(f5, "xyz%")) +f5 != f6 +f1 BETWEEN AND "a_b%" +f5 IN (10, 1, 1e10, "hello world", "xyz%") +f2 = -1e-10 +f1 = ' ') +f2 BETWEEN AND 0 +((f5 > 999999999999) AND (f4 IN (-2.7, "xyz%", -1e-10))) OR (hasAll(f6, f1)) +f5 >= 1 +hasAny(f4, TRUE) +f1 NOT +() +f4 BETWEEN "\n" +f5 LIKE +f2 IN ('test', f2) +hasToken(f3) +f2 IN (0.0000000001) +f1 IN (3.14, -.5, 'test', "%wild%") +f1 BETWEEN -3.5e-2 AND 10 +f4 BETWEEN -.5 AND 1e10 +() +(hasAll(f3, "a_b%")) OR (f2 BETWEEN 'O''Reilly' AND -3.5e-2) +f2 IN (-2.7, "\t", .5) +NOT +f1 = NULL +f6 IN (' ', 6.02e23) +hasAll(f6, .5) +f4 BETWEEN AND .5 +f5 LIKE 'test' +hasToken(f2, 3.14) +() +() +f6 IN ("a_b%") +f6 REGEXP "%wild%" +(f6 IN (f4, f6)) AND (f5 == ' ') +f2 REGEXP 'O''Reilly' +hasToken(f4) +f2 REGEXP "%wild%" +f3 NOT REGEXP "abc" +has(f5, -1) +hasAll(f5, f4) +f5 <> "%wild%" +(hasToken(f5, 'test')) OR (f6 BETWEEN 'test' AND "\n") +hasAll(f5, f3) +f2 NOT REGEXP ' ' +(f5 IN (.5, 10)) AND (f1 BETWEEN f2 AND f3) +has(f6, -.5) +hasToken(f2, 3.14) +f5 IN ('O''Reilly', "xyz%", "%wild%", -2.7, "hello world") +f2 = +hasToken(f3, .5) +f5 LIKE +hasAll(f1, "a_b%") +f4 NOT ILIKE 'test' +AND f3 = "a_b%" +f6 == .5 +has(f1, "abc") +f5 = "abc") +f6 > -2.7 +f6 IS NULL +f3 >= 999999999999 +f2 NOT CONTAINS "xyz%" +f5 IN ("\t", "abc", 999999999999, ' ', "abc") +hasToken(f1) +f4 IN ("abc", f5, 1e10, ' ') +f1 <= 'test' +(f6 != f6) AND (f4 ILIKE ' ') +f5 BETWEEN TRUE AND "\n" +f2 IN (1e10, 3.14, "\n") +f3 BETWEEN -2.7 +(f3 LIKE "a_b%") AND (f2 = -.5) +f1 BETWEEN "xyz%" AND 6.02e23 +f4 BETWEEN -.5 AND f6 +f3 IN [1 2 3] +f4 >= "\t" +((f5 != 1e10) OR (hasToken(f5, "hello world"))) OR (f1 IN ("%wild%", f3)) +f1 NOT CONTAINS 'O''Reilly' +f1 = 'unclosed +hasToken(f1, 999999999999) +f6 NOT REGEXP '' +f2 BETWEEN "\t" AND 'test' +f4 = 'unclosed +(f2 != "\t") AND (f3 IN (-1, "xyz%")) +f2 BETWEEN 3.14 AND -1 +f4 IN (-.5, 0, "hello world", "hello world") +f6 >= "hello world" +f2 BETWEEN "hello world" AND '' +() +f1 OR OR 999999999999 +f3 ILIKE "hello world" +f6 IN (1e10, TRUE) +has(f5, -1) +f5 CONTAINS "xyz%" +f1 BETWEEN f4 AND 0 +f4 ILIKE "abc" +f2 IN [1 2 3] +f5 LIKE "abc" +f5 NOT REGEXP "abc" +f2 ILIKE "a_b%" +f5 IS NULL +f1 <> "\n" +(f1 CONTAINS ' ') AND (f1 BETWEEN 1 AND "xyz%") +((f4 == -3.5e-2) AND (f1 NOT CONTAINS 'O''Reilly')) OR (f4 BETWEEN 10 AND 6.02e23) +AND f3 = -.5 +f5 BETWEEN "hello world" AND 3.14 +((hasAll(f3, f2)) OR (f4 BETWEEN 1 AND 999999999999)) AND (f6 NOT REGEXP "a_b%") +f4 BETWEEN TRUE AND 3.14 +has(f3, -.5) +f6 IN (' ', .5, "\n") +f1 == "a_b%" +() +hasToken(f6, -.5) +f1 LIKE '' +f4 IN (1, f6, 0.0000000001) +(f5 == -1) OR (f5 IN (-.5, 3.14, f1, 'O''Reilly', -1)) +hasAny(f6, "\t") +(f2 BETWEEN "a_b%" AND 0.0000000001) OR (hasToken(f3, "hello world")) +f5 != .5 +hasAll(f4, "hello world") +f6 BETWEEN 0 AND -3.5e-2 +f2 BETWEEN f5 AND 6.02e23 +() +f3 BETWEEN "\t" AND f3 +f5 = 0 +has(f5, .5) +f1 LIKE "\n" +f1 BETWEEN AND "\n" +hasAll(f6, 'O''Reilly') +f5 <= f4 +f4 = "abc") +f6 <> f2 +f5 == "a_b%" +hasAll(f5, "xyz%") +f5 BETWEEN 'O''Reilly' AND 6.02e23 +f2 = "hello world") +f3 CONTAINS "\n" +f3 IN ("hello world", "\t", f1, f2, 6.02e23) +f1 != "hello world" +f6 IN (,) +f2 BETWEEN f6 +f4 IS NULL +f1 IN (-2.7, -.5) +f1 IN (.5, TRUE, -10, 1e10, "a_b%") +f3 <= .5 +f1 BETWEEN AND f3 +hasAny(f2, f3) +f6 != -1e-10 +f2 = +f4 NOT REGEXP 'test' +f6 = f2) +f6 OR OR "%wild%" +(f6 BETWEEN 'O''Reilly' AND f5) AND (f2 IN (TRUE, 3.14, f5)) +f5 IS NULL +f5 IN () +f5 === f2 +(f1 IN (TRUE)) AND (hasToken(f3, 'O''Reilly')) +(f2 = 'O''Reilly' +f2 == "%wild%" +f4 NOT +hasToken(f6) +(f3 BETWEEN 0.0000000001 AND "%wild%") OR (hasAny(f5, -10)) +f5 IN (1, "\t", "\t", "xyz%") +f1 IN (,) +(f6 != "abc") AND (f6 <= 10) +(f1 NOT ILIKE ' ') AND (f6 <= 10) +f2 NOT +f2 < f4 +AND f2 = f5 +f3 = +f2 >= "%wild%" +has(f2, 1) +f5 BETWEEN f2 AND 0 +f5 LIKE 'test' +f1 CONTAINS "abc" +hasToken(f1, 6.02e23) +f6 BETWEEN "\n" AND "xyz%" +f5 <> f5 +f1 BETWEEN f2 AND TRUE +f1 != 'test' +((f3 IN (f6, -1, 0, -3.5e-2)) OR ((f2 BETWEEN 999999999999 AND "\t") AND (f1 BETWEEN "\t" AND -1))) OR (hasToken(f6, ' ')) +f3 LIKE +f6 IN (3.14, "a_b%") +f4 < 6.02e23 +f4 = 999999999999 +f6 < -3.5e-2 +f6 BETWEEN .5 AND 0.0000000001 +f5 IN (f6, -10, 0.0000000001) +f1 IN (-10, 0, "a_b%", 0.0000000001) +f3 == "\n" +f6 IN ("\n", 'test', "\t", "abc", 0.0000000001) +hasAny(f3, 'O''Reilly') +f1 > 1 +f3 BETWEEN f5 +hasAll(f3, .5) +f4 >= "%wild%" +(f2 = "xyz%" +f6 IN (,) +(f4 IN (f3, 6.02e23, -1, ' ')) AND (f1 LIKE ' ') +has(f4, '') +f6 IN (0.0000000001, f4, 10, 999999999999) +f3 IN (1, 1e10) +f3 === f6 +f3 IN ("\t", 6.02e23, f2, 1e10) +f1 IN (TRUE, f1, "xyz%", -.5) +((f4 BETWEEN -10 AND 999999999999) OR (f6 BETWEEN "\t" AND f4)) OR ((f5 IN (999999999999, f1)) OR (hasToken(f1, 1))) +f5 = +f6 != f4 +f6 NOT LIKE "a_b%" +f4 REGEXP "abc" +((f4 LIKE ' ') OR (f6 NOT REGEXP "a_b%")) OR (f4 BETWEEN -10 AND 1) +hasAny(f3, 1e10) +f6 ILIKE ' ' +f1 BETWEEN 1e10 AND 1e10 +f2 LIKE +f1 NOT +f4 IN [1 2 3] +(f2 = "hello world" +f1 <= 1 +f4 BETWEEN "\t" +f3 <> 999999999999 +f2 REGEXP 'O''Reilly' +f1 BETWEEN f2 AND -3.5e-2 +f4 NOT ILIKE '' +f5 BETWEEN -1 AND "\t" +AND f3 = 10 +f4 BETWEEN 1e10 AND 3.14 +hasToken(f1, 0.0000000001) +f3 <= 10 +f6 CONTAINS ' ' +f2 IN (-3.5e-2, 'O''Reilly', ' ', 999999999999) +f5 IN ('', 1, f2, 0, f6) +f4 BETWEEN AND 'O''Reilly' +(f5 != -1) AND (f1 BETWEEN '' AND 'O''Reilly') +f2 = 'unclosed +f1 BETWEEN -3.5e-2 AND "abc" +f2 > "%wild%" +f1 <> "%wild%" +(f2 <= f6) OR ((hasAny(f2, -1e-10)) AND (f2 IN (-3.5e-2, f1, -.5, 0))) +f1 IN [1 2 3] +f5 IN ("a_b%", -.5, 10, .5) +f4 NOT ILIKE "xyz%" +f5 IN ("hello world", -3.5e-2, -10) +f4 === 3.14 +f6 <> -2.7 +f6 BETWEEN "\t" AND -.5 +f3 = +AND f4 = f2 +f2 BETWEEN -2.7 AND -10 +f3 BETWEEN ' ' AND 0 +f2 IN (-2.7, -2.7) +(hasToken(f4, f3)) AND (f4 IN (-.5, '', "xyz%", -.5, 6.02e23)) +f5 = NULL +f6 CONTAINS "\n" +f2 BETWEEN -3.5e-2 AND f1 +f5 IN (-3.5e-2) +((f3 LIKE '') OR ((f6 IN (-1e-10, -3.5e-2, "a_b%", f1, f4)) AND (f2 LIKE ''))) OR (f4 ILIKE "\t") +f3 BETWEEN 'O''Reilly' AND 999999999999 +f6 OR OR -3.5e-2 +f5 != ' ' +(f2 REGEXP ' ') AND (hasAny(f4, "xyz%")) +f6 IN [1 2 3] +has(f1, TRUE) +(f2 <= "\t") AND (f3 IN ('')) +f1 BETWEEN f4 AND 'test' +f6 <> -10 +f5 <> "abc" +f5 BETWEEN 1e10 AND -.5 +f1 NOT LIKE "\n" +f6 = TRUE +f5 LIKE +NOT +f1 IN ("a_b%") +f2 BETWEEN "hello world" AND f1 +f6 ILIKE 'test' +f2 BETWEEN TRUE AND .5 +hasAll(f3, 3.14) +f3 = 'unclosed +f4 CONTAINS '' +f1 CONTAINS 'test' +f5 BETWEEN f1 +has(f4, 3.14) +f4 IS NULL +f1 IN (-3.5e-2, 0.0000000001, "hello world", ' ') +f2 IN ("\n", "xyz%", f2, 0, -3.5e-2) +f5 IN (0.0000000001, "\n", -.5) +hasAll(f1, f3) +hasAll(f4, -10) +f5 > 0.0000000001 +f6 IN (f4, "hello world", 1e10) +(f3 BETWEEN 1 AND -1) AND (f5 NOT LIKE 'O''Reilly') +f3 >= TRUE +f2 IN (f6, "xyz%", "hello world", 999999999999) +f6 CONTAINS "xyz%" +f6 NOT REGEXP "%wild%" +f4 ILIKE "\t" +AND f2 = -10 +(has(f1, 'O''Reilly')) OR (f6 >= "\n") +f3 BETWEEN TRUE AND f1 +f2 <> f2 +f5 NOT ILIKE 'test' +f6 NOT CONTAINS "\t" +f4 NOT LIKE '' +f2 IS NULL +hasAll(f4, 999999999999) +f6 IN (-1, "\n", f4, 999999999999, TRUE) +(((hasToken(f4, 1)) OR ((f1 < -.5) AND (f1 >= 10))) OR ((f6 NOT LIKE '') OR ((f4 = TRUE) OR (f2 != 10)))) AND (f2 LIKE ' ') +hasAny(f5, f6) +f1 IS NULL +(hasAll(f4, "xyz%")) AND (f5 REGEXP "hello world") +(hasAny(f5, 3.14)) AND (f3 IN (1, .5, 'test', ' ', TRUE)) +f5 ILIKE "xyz%" +hasAny(f4, 0) +hasAny(f5, "hello world") +hasAll(f6, "abc") +(f6 = f6 +(((f6 IN (10, -2.7, "\n", "xyz%", 6.02e23)) AND (f1 IN (f2, -1))) AND (f1 ILIKE "%wild%")) OR (f4 = 999999999999) +(f5 == f3) AND (f2 ILIKE "\n") +NOT +hasAny(f3, f3) +f1 <= 0 +has(f6, "\n") +hasAny(f2) +f1 ILIKE "%wild%" +f3 IN (999999999999, "a_b%", "\n") +f6 BETWEEN "xyz%" AND "%wild%" +f6 === 3.14 +f2 IN (1, 3.14, 6.02e23, 0.0000000001) +f1 BETWEEN 1e10 AND "hello world" +f6 LIKE 'test' +(f2 < "abc") OR (f2 IN (-1, ' ', ' ')) +f3 NOT LIKE '' +f3 OR OR f6 +f5 = 'unclosed +AND f2 = -1 +f5 IN (1e10, -.5, 10, "xyz%") +hasAny(f4, -3.5e-2) +f5 === "\n" +f4 IN (6.02e23, "abc", 999999999999, "a_b%", f4) +f4 IN (,) +f5 IN (10, "xyz%") +f2 = +f3 < 10 +(f4 ILIKE "\n") AND (f6 IN (-1, 'test')) +hasAny(f1, 'O''Reilly') +f5 BETWEEN f5 AND "a_b%" +(f2 LIKE "\n") OR (((f2 IN ('test', 0)) OR (hasAny(f6, 10))) AND (f3 BETWEEN 0.0000000001 AND 1)) +f4 CONTAINS "\n" +f1 IN () +f2 IN (f2) +f1 CONTAINS "\n" +f2 REGEXP "abc" +f4 IN (.5, -3.5e-2, "a_b%") +f6 <> f4 +f4 IN (0) +f4 NOT ILIKE "\t" +f3 LIKE 'test' +hasAny(f6, -1) +f5 IN (-10, -1e-10, f4, -1e-10, 999999999999) +f3 BETWEEN 1e10 AND 'test' +f3 IS NULL +hasAny(f4, 0.0000000001) +f1 >= TRUE +hasToken(f1, -1) +f6 OR OR ' ' +f1 IN (-10, ' ', f6, 0, ' ') +((f3 >= "xyz%") OR ((f6 NOT REGEXP 'test') AND (f1 NOT CONTAINS ' '))) AND (f3 = "abc") +f1 === 'O''Reilly' +hasAny(f1, "\n") +f6 BETWEEN -1 AND "abc" +hasToken(f5, "hello world") +(f6 >= 0) AND (f3 ILIKE "\n") +f4 < f1 +f2 BETWEEN "abc" AND "%wild%" +f2 BETWEEN 10 AND -1e-10 +f3 REGEXP "abc" +hasToken(f1, f5) +(has(f4, -3.5e-2)) AND ((f5 == 'test') AND (f1 <> 6.02e23)) +(f4 = "%wild%" +(f5 <= -1) OR (f6 ILIKE "hello world") +f5 BETWEEN f4 AND 0 +f3 IN ('test', f4, 10) +f6 IN [1 2 3] +f5 BETWEEN "hello world" AND TRUE +f4 = +f2 <= -10 +f1 IN ('', f5) +hasToken(f4) +f2 CONTAINS "abc" +f6 BETWEEN 3.14 AND f1 +f5 == 0 +AND f5 = .5 +f3 IN (-1e-10, f3) +() +f4 BETWEEN ' ' AND f5 +f6 IN (,) +NOT +f3 IN ("\t", -3.5e-2, -3.5e-2, '') +f5 NOT CONTAINS "abc" +f4 LIKE 'test' +f4 IN (,) +(f4 LIKE "hello world") AND (f1 IN (f6, '', 1)) +f6 BETWEEN 0 AND -10 +((f4 NOT LIKE "a_b%") OR (f3 >= 10)) OR (f2 BETWEEN "xyz%" AND 'O''Reilly') +f5 BETWEEN "a_b%" AND 'O''Reilly' +f1 IN ("xyz%", '', 999999999999, -2.7) +f6 IN (10, "a_b%", f3, .5) +(((hasToken(f4, 'O''Reilly')) OR (hasToken(f2, "%wild%"))) OR (f5 <> -1e-10)) OR ((f3 NOT CONTAINS "%wild%") AND (f6 IN (10, 1e10, "abc", f4))) +f6 OR OR 1e10 +f2 BETWEEN -1e-10 AND 6.02e23 +hasToken(f6, 999999999999) +hasAll(f2) +f4 === 1e10 +hasAny(f3, -1) +(f5 == "xyz%") AND (f4 <> 0.0000000001) +f2 = +f4 BETWEEN AND "%wild%" +(f4 = 1 +f2 IS NULL +f1 BETWEEN TRUE AND f6 +f4 <= 'O''Reilly' +f4 = 999999999999 +f2 <= -1 +f2 IN (f6, 'O''Reilly', -.5, 'test', 3.14) +f6 >= 'O''Reilly' +f4 BETWEEN "hello world" AND 1 +f4 NOT REGEXP '' +f4 ILIKE "a_b%" +f4 != 0 +f2 IN (,) +f3 IN (f5, 999999999999, -2.7, 999999999999, f5) +hasAll(f6, -10) +(f1 NOT REGEXP "a_b%") AND (f5 IN (6.02e23, TRUE)) +AND f4 = 999999999999 +(has(f6, -2.7)) OR (hasAll(f3, 0.0000000001)) +f6 == 1e10 +f6 ILIKE "hello world" +NOT +f5 != -.5 +hasToken(f6, 10) +has(f3, "\n") +f6 NOT LIKE 'test' +f5 IN ("\t", 'O''Reilly', f3) +hasAll(f5, "\t") +(hasToken(f4, "hello world")) OR (f6 BETWEEN "abc" AND 0) +f4 IN () +f1 BETWEEN -2.7 AND TRUE +f6 IN (f6, TRUE, "hello world", "%wild%") +hasAll(f4, "a_b%") +f3 = +f5 NOT CONTAINS "a_b%" +f5 ILIKE 'O''Reilly' +f1 IN ("\n", ' ', "xyz%", "a_b%") +f3 BETWEEN 'O''Reilly' AND "xyz%" +hasToken(f5, 'test') +f2 BETWEEN TRUE AND f1 +f3 BETWEEN f4 AND -1 +f1 NOT ILIKE "%wild%" +hasAll(f3, 6.02e23) +f5 NOT +f3 BETWEEN 0 AND f1 +hasToken(f4, f5) +f3 IN ("hello world", .5) +f5 REGEXP '' +f1 IN (999999999999, ' ', "\n", f5) +hasAll(f1, f5) +f5 BETWEEN "xyz%" AND 1 +f5 BETWEEN AND -1 +AND f1 = -.5 +f5 >= f3 +hasToken(f4, '') +f6 < -10 +f6 REGEXP "\t" +f2 BETWEEN 0 AND f6 +(hasToken(f3, f6)) OR (f5 <= -3.5e-2) +f2 BETWEEN f1 AND 1 +f6 IN ("\n", "hello world", -3.5e-2, f3, TRUE) +f3 IN [1 2 3] +(f3 == 3.14) OR (hasAny(f5, 'test')) +AND f4 = "%wild%" +f2 BETWEEN f4 AND .5 +has(f5, 6.02e23) +() +f4 NOT REGEXP 'O''Reilly' +f4 NOT CONTAINS "a_b%" +has(f5, -1) +f6 BETWEEN AND 999999999999 +f3 IN ("\n", -3.5e-2, "\t", 999999999999, f6) +hasToken(f4, 999999999999) +f1 BETWEEN f4 AND "\t" +f5 BETWEEN "\t" AND -2.7 +(hasAll(f1, 3.14)) AND (f5 BETWEEN "\n" AND -2.7) +f1 BETWEEN f5 AND 0 +f4 IN [1 2 3] +f5 NOT ILIKE 'test' +(((has(f4, "abc")) OR (f4 >= "a_b%")) OR (hasToken(f5, 0.0000000001))) AND (f3 IN (0, f5, f2, "abc")) +f6 BETWEEN 999999999999 AND -2.7 +f6 IN ("a_b%") +f3 != 3.14 +hasAny(f5, "hello world") +f3 IN ('O''Reilly') +hasAny(f5, -2.7) +f3 BETWEEN AND -10 +has(f4, "a_b%") +f5 NOT LIKE "abc" +f4 NOT CONTAINS '' +f2 BETWEEN 'O''Reilly' AND "hello world" +hasAll(f3, TRUE) +f5 = -10) +f5 BETWEEN "a_b%" AND f4 +f5 NOT ILIKE "xyz%" +f3 NOT CONTAINS "hello world" +(f6 BETWEEN f2 AND 0.0000000001) AND (hasAny(f4, -1)) +f3 > 'O''Reilly' +f2 BETWEEN 1e10 AND 'O''Reilly' +(f5 > "xyz%") AND (hasToken(f4, 1)) +f2 OR OR "\n" +hasAll(f3, 3.14) +f4 === f1 +f1 NOT +f6 LIKE "\n" +hasToken(f2, f2) +(hasAny(f6, "\t")) OR ((f2 BETWEEN 1 AND 6.02e23) OR (((f5 <= 1e10) AND (f5 <> TRUE)) OR (f4 NOT LIKE "\n"))) +f4 CONTAINS "xyz%" +f6 IN ("abc", "hello world", 1e10, -1) +f2 BETWEEN 0.0000000001 AND "hello world" +has(f1, 999999999999) +f5 NOT ILIKE "%wild%" +has(f6, -2.7) +f5 NOT LIKE "a_b%" +f3 IN (.5, "hello world", 'test', 1) +f2 BETWEEN ' ' AND -10 +f3 CONTAINS 'O''Reilly' +f1 NOT LIKE '' +f2 CONTAINS 'test' +hasToken(f3, "%wild%") +f6 BETWEEN 1e10 AND f5 +f3 NOT LIKE 'O''Reilly' +f2 > 0 +f6 = NULL +hasAny(f5, 0) +f2 BETWEEN -10 AND "xyz%" +f3 IN () +(f5 NOT CONTAINS "\n") OR (f1 BETWEEN -.5 AND 0) +hasAll(f3, "abc") +(f1 NOT ILIKE "abc") OR (has(f1, -2.7)) +hasAny(f4, 3.14) +hasAll(f1, 3.14) +() +f1 LIKE "\n" +(f4 IN (-.5, "%wild%", f3)) AND (f6 LIKE "hello world") +(f5 = -1e-10 +f2 BETWEEN 10 AND 1e10 +f4 LIKE +f1 BETWEEN "\t" AND TRUE +f3 == "xyz%" +hasAny(f5, 1) +f4 NOT ILIKE 'test' +hasAny(f5) +f5 LIKE +f6 IN [1 2 3] +f6 BETWEEN 1e10 AND '' +f1 NOT ILIKE "hello world" +hasAll(f3, TRUE) +f2 IN ('O''Reilly', 1e10) +f3 IN (f2, f1, 0, 1e10, "xyz%") +f4 NOT ILIKE 'O''Reilly' +f6 > "abc" +f6 NOT CONTAINS "\t" +f2 BETWEEN 0.0000000001 +f4 NOT REGEXP "a_b%" +f3 >= "\n" +f3 = NULL +(f3 = f2 +f6 IN ("%wild%", 999999999999) +((f6 BETWEEN "%wild%" AND "abc") AND (f2 IN (f5, 6.02e23, 'O''Reilly', -2.7))) OR (f1 ILIKE "\t") +(f2 NOT ILIKE "a_b%") OR (f6 BETWEEN 'test' AND TRUE) +hasToken(f6, "abc") +f4 == "\t" +f3 BETWEEN f4 AND "abc" +has(f6, TRUE) +f6 BETWEEN f6 AND 3.14 +hasAny(f6, f3) +f1 BETWEEN f5 AND 999999999999 +f1 NOT CONTAINS "hello world" +f1 IN [1 2 3] +f1 BETWEEN 'test' AND "\t" +(f5 = 6.02e23 +f3 = +has(f3, .5) +f6 CONTAINS 'O''Reilly' +f5 <= f2 +(f1 = -1 +f5 IN (.5) +(hasAll(f5, -1)) AND (f2 BETWEEN 0.0000000001 AND "hello world") +f2 IN [1 2 3] +(f6 = -10) OR (f5 NOT ILIKE "abc") +f5 IN (-2.7) +f5 IN (3.14, 1, 10, 'O''Reilly', -3.5e-2) +has(f1) +f2 < f6 +f3 = -10) +f5 NOT LIKE ' ' +f5 IN () +f1 = +has(f5, 'test') +f5 BETWEEN f3 AND 'O''Reilly' +f6 NOT LIKE "\n" +f4 NOT REGEXP ' ' +f2 NOT LIKE 'test' +f1 >= 'O''Reilly' +f6 <= .5 +has(f3) +f2 IN (10, TRUE, f1) +f2 === TRUE +f4 IS NULL +hasAll(f6, ' ') +hasAny(f3, 10) +f5 > f1 +(f3 BETWEEN '' AND f6) OR (f2 BETWEEN f2 AND 3.14) +f4 ILIKE "\t" +f1 IN ("\n", 0, f4) +f1 < "hello world" +f1 BETWEEN "\n" AND f6 +f2 REGEXP "abc" +f5 LIKE +has(f2, f4) +f5 BETWEEN .5 AND .5 +(f2 BETWEEN 6.02e23 AND 6.02e23) AND (f5 BETWEEN 6.02e23 AND 'test') +f4 IN (6.02e23, -1e-10, ' ') +f5 LIKE +f6 == "abc" +f3 <> "a_b%" +f4 = NULL +f5 >= "abc" +has(f1, 1) +f1 LIKE +f3 IN () +(has(f2, "\n")) AND (f4 IN (1, 0.0000000001, f3, .5, 6.02e23)) +((f3 NOT ILIKE "%wild%") AND (f3 IN ("a_b%", ' ', f5, '', 'O''Reilly'))) AND (hasAny(f3, 0)) +f5 IN () +f3 CONTAINS ' ' +f3 NOT REGEXP ' ' +() +has(f5, -.5) +(hasAny(f3, f4)) OR (hasAny(f5, "\t")) +f4 IN (1e10, "a_b%", 0, 'test') +f6 BETWEEN 6.02e23 AND "xyz%" +f5 IN (-1) +f6 ILIKE 'O''Reilly' +f3 IN ("\t", -2.7) +f3 IN ('test', 1, "\n", 1e10) +hasAll(f5) +f2 BETWEEN ' ' +f5 BETWEEN "\n" AND 0.0000000001 +f2 <= "\t" +(hasToken(f1, -3.5e-2)) AND (f3 ILIKE "\n") +f1 BETWEEN 'O''Reilly' AND 3.14 +has(f2, ' ') +hasToken(f2, "\n") +f1 CONTAINS 'test' +() +f3 IN [1 2 3] +f2 BETWEEN '' AND 0.0000000001 +f1 LIKE "abc" +f2 > f1 +f2 BETWEEN "%wild%" AND "\n" +((f5 > ' ') OR (f6 >= 6.02e23)) AND (f3 > 999999999999) +f1 BETWEEN "\n" AND f3 +hasAll(f1, "%wild%") +f6 REGEXP 'test' +(hasAll(f3, -1e-10)) AND (f6 >= f5) +f6 CONTAINS "a_b%" +f6 BETWEEN f4 AND TRUE +f5 != 3.14 +hasToken(f6) +f4 >= f2 +((f3 BETWEEN "abc" AND -.5) AND (f6 BETWEEN f5 AND .5)) OR (has(f2, -.5)) +(((f4 IN (f5, 3.14)) AND (f2 IN ('O''Reilly', -1e-10, 1, -1, 1))) AND (f3 NOT ILIKE "\n")) AND (f6 BETWEEN -10 AND 999999999999) +f3 CONTAINS "%wild%" +f4 BETWEEN "xyz%" AND -2.7 +hasToken(f2, TRUE) +f3 <> -.5 +f3 = "\n" +f2 IN () +(f4 = f5 +f5 NOT CONTAINS "%wild%" +f5 REGEXP "\n" +f3 BETWEEN 'test' AND '' +f1 BETWEEN 0 +f1 IN (10, TRUE) +f4 NOT +f2 IN (-.5) +f4 IN (0, 1e10, 6.02e23) +f3 OR OR f5 +has(f3, f3) +f6 BETWEEN -.5 AND 3.14 +hasAny(f6, "abc") +(f6 BETWEEN 10 AND "hello world") OR (f6 <= f5) +(f4 <> TRUE) AND ((hasAny(f4, 0)) AND (f2 != 1)) +f1 = 'unclosed +f6 CONTAINS "abc" +f5 NOT +f4 IN (-.5, 1, '', TRUE, "hello world") +f1 <> 6.02e23 +f1 IN (999999999999, '', 10, 0, -10) +f3 IN (.5, f4, ' ', 10) +f2 = 'test' +f1 < 10 +f3 <= "\n" +f3 = NULL +f3 IN (10, -1e-10, -1, 0, -2.7) +f5 BETWEEN AND "a_b%" +hasAny(f1, f3) +f6 OR OR f1 +f4 CONTAINS "xyz%" +hasToken(f4, f2) +(hasAny(f2, -3.5e-2)) OR (f4 = "a_b%") +f3 BETWEEN ' ' +f5 BETWEEN '' AND "abc" +f2 BETWEEN f6 AND -3.5e-2 +f4 ILIKE "xyz%" +(f5 IN (-10)) OR (hasToken(f3, f1)) +has(f2, -1e-10) +hasAny(f3, -3.5e-2) +f4 OR OR "xyz%" +hasAny(f6) +hasAny(f4, 0) +hasAny(f4, 0) +f5 BETWEEN -3.5e-2 AND f4 +NOT +AND f2 = 10 +f2 IN ('test', 'test') +(f6 IN (f2, f4, -2.7, f1)) AND ((hasToken(f6, -2.7)) OR (f6 IN (f1, ' ', "\t", "\t", f3))) +f5 <= "abc" +f1 IN ('') +f2 IN ('', -1e-10, f3, -1e-10) +f2 OR OR 0 +f6 NOT ILIKE "%wild%" +f3 >= .5 +f1 IN (-1) +(f1 LIKE '') AND (f4 BETWEEN ' ' AND 999999999999) +f3 NOT CONTAINS '' +f2 LIKE +f4 NOT LIKE 'O''Reilly' +(f6 CONTAINS 'O''Reilly') AND (f1 BETWEEN 999999999999 AND 3.14) +() +hasToken(f2, -.5) +f1 != 1 +(f3 <= 1) OR (f5 = "%wild%") +f4 BETWEEN .5 AND "\t" +f2 = +f1 = 10 +hasAny(f5, -10) +has(f1, 'O''Reilly') +f5 BETWEEN AND -1 +f1 BETWEEN "xyz%" AND 0.0000000001 +f4 NOT LIKE 'test' +f4 OR OR -.5 +f5 IN () +f6 IN (999999999999, "abc", 0, f1, f5) +f5 = -.5) +f4 NOT REGEXP "\t" +(f4 CONTAINS ' ') AND (hasToken(f4, "\t")) +f2 === "abc" +f2 ILIKE "hello world" +f4 LIKE "\n" +f4 BETWEEN "\t" AND 3.14 +AND f3 = 1e10 +hasToken(f4, f3) +f5 IN ("\n", 'test', 1, -1e-10) +(f3 = f2 +hasAny(f5, -10) +NOT +f6 > -1 +hasAll(f2, "hello world") +(hasToken(f4, ' ')) OR (f6 <> 1) +NOT +(f3 ILIKE "\n") AND (f6 NOT REGEXP "hello world") +f2 BETWEEN AND 0.0000000001 +f6 NOT ILIKE "abc" +f3 = NULL +f6 OR OR "hello world" +f6 NOT +NOT +(hasAll(f2, 0.0000000001)) OR ((f6 ILIKE "%wild%") OR (f4 BETWEEN "\t" AND 'O''Reilly')) +f2 BETWEEN 1 AND f3 +has(f4, f6) +f3 BETWEEN "xyz%" +() +f2 LIKE "abc" +f2 IN (-1e-10, -1, f3, f2, 0.0000000001) +f4 = -10) +has(f3, ' ') +f6 BETWEEN 0 AND 'O''Reilly' +f1 IS NULL +((f2 BETWEEN f5 AND "%wild%") AND (f6 == .5)) AND (f6 BETWEEN ' ' AND 1) +f4 BETWEEN ' ' AND 1e10 +f3 = NULL +f5 BETWEEN ' ' AND ' ' +f3 <= -1 +f6 OR OR f4 +(((f2 NOT CONTAINS "hello world") AND (f4 NOT ILIKE 'test')) OR (f4 IN (-.5))) AND ((f3 CONTAINS "hello world") OR (f1 IN (f1, 1))) +hasToken(f1, "\t") +has(f5, f1) +f5 <= "abc" +f5 BETWEEN "%wild%" AND -10 +f1 REGEXP "a_b%" +f6 IN (6.02e23, -.5, TRUE, f2, 0.0000000001) +f2 <> "hello world" +AND f5 = 'O''Reilly' +f4 BETWEEN 'O''Reilly' AND '' +(hasAny(f2, -2.7)) AND (f2 IN ('', '', f2, "\n")) +has(f5, .5) +f5 IN ('test') +f1 BETWEEN -1e-10 AND 'O''Reilly' +hasAny(f3, 3.14) +AND f6 = f4 +f6 >= -.5 +f1 IN (999999999999, -3.5e-2, f6, .5) +f2 IN [1 2 3] +f5 NOT LIKE "a_b%" +(f4 NOT LIKE "abc") OR (f5 BETWEEN f6 AND f2) +f6 REGEXP "a_b%" +f6 = +f2 OR OR -1 +f2 NOT +(f4 IN (' ', 'O''Reilly', -10, 6.02e23)) AND (f5 NOT LIKE "abc") +f5 BETWEEN -.5 AND f2 +f3 == "abc" +f1 NOT LIKE '' +f1 BETWEEN -2.7 AND "abc" +hasAll(f2, "abc") +hasAny(f6, -10) +f6 = NULL +f6 BETWEEN AND "\n" +f2 IN ("xyz%") +f1 >= 0 +f2 REGEXP '' +f6 > -2.7 +f1 NOT REGEXP "xyz%" +AND f5 = "xyz%" +f3 IN (.5, 1e10, f3, f1) +f4 BETWEEN f2 AND .5 +(hasAny(f4, f1)) AND (hasToken(f3, -2.7)) +f4 NOT CONTAINS '' +f3 > f5 +f2 >= 'test' +(f1 = 'O''Reilly' +f2 IS NULL +f6 < 1e10 +f3 != 'test' +f5 <= TRUE +f3 NOT CONTAINS "abc" +f4 = +hasToken(f2, 0.0000000001) +f6 BETWEEN 3.14 AND ' ' +f6 BETWEEN 6.02e23 AND .5 +f4 BETWEEN 10 AND "\n" +f3 BETWEEN -10 AND -1 +f1 === -2.7 +f5 <> 1 +f4 <= 10 +f4 === "xyz%" +f2 OR OR TRUE +f5 == 0 +f2 NOT REGEXP "%wild%" +f1 === -2.7 +f1 CONTAINS "\n" +f2 NOT CONTAINS '' +f5 <> 10 +f6 BETWEEN 10 AND .5 +f6 LIKE +(f1 NOT ILIKE "xyz%") OR (hasAll(f5, "hello world")) +f3 = "abc" +f5 BETWEEN ' ' AND 'test' +f5 IN ("%wild%", TRUE) +f2 BETWEEN 999999999999 +f1 IN (1e10, "a_b%", -10, "xyz%", f5) +f2 IN [1 2 3] +f3 IN (999999999999) +hasAll(f3, 0.0000000001) +f2 LIKE "\n" +f5 CONTAINS '' +f5 IN (f1) +f3 IN (f3, -.5, "hello world", -1, 1e10) +f4 NOT ILIKE "a_b%" +f4 <= f6 +f2 BETWEEN f4 AND -1e-10 +f4 === -.5 +(((f6 <> 10) OR ((f2 <> f1) OR (f3 = -2.7))) AND ((f3 IN ('test', "hello world")) AND ((f1 != "hello world") AND (f3 = 'O''Reilly')))) OR (f3 NOT LIKE "xyz%") +has(f4, "%wild%") +f1 IN ('test', -2.7, 'O''Reilly', f5, ' ') +has(f5, -1e-10) +f3 IN ('O''Reilly', "a_b%") +f6 BETWEEN -10 +hasAny(f3, 1e10) +f2 BETWEEN f4 AND "\n" +f5 CONTAINS "\n" +f5 BETWEEN ' ' AND 0.0000000001 +f1 REGEXP "xyz%" +f6 = +(hasAny(f6, "hello world")) OR (f2 BETWEEN 'O''Reilly' AND 0.0000000001) +NOT +f3 IN ('test', "abc") +f4 BETWEEN -.5 AND "%wild%" +f3 NOT ILIKE "a_b%" +f1 = f1) +f3 BETWEEN -2.7 AND f4 +f3 >= .5 +f5 = 'unclosed +f3 = 1) +f6 LIKE "%wild%" +f3 >= 3.14 +f5 BETWEEN "xyz%" AND f2 +f4 NOT REGEXP "%wild%" +f1 IN (-10, f6, f1) +f6 = 'unclosed +f3 = TRUE) +() +f6 ILIKE "a_b%" +f1 = f5) +f3 NOT LIKE "\t" +f5 BETWEEN 'test' AND "abc" +f4 IN () +f2 >= "\n" +f5 <> 0 +f6 BETWEEN AND f6 +f4 IN (,) +f6 BETWEEN "xyz%" AND "\t" +hasAny(f4, 0.0000000001) +f6 != -1e-10 +f6 CONTAINS "xyz%" +(f2 NOT LIKE ' ') OR (f3 IN (f3, f1, 1e10, TRUE)) +f4 NOT +f3 = 'unclosed +f3 LIKE 'test' +f4 NOT CONTAINS 'O''Reilly' +f1 BETWEEN f4 AND TRUE +f3 >= ' ' +f1 > f4 +f6 OR OR "hello world" +(f2 BETWEEN 'test' AND "xyz%") OR (f5 BETWEEN "%wild%" AND f1) +f2 IN (-.5, TRUE) +f6 = -1 +f5 == -10 +f3 BETWEEN '' AND TRUE +hasToken(f6, "a_b%") +f1 IN () +f4 IN (f1, 3.14, f6, TRUE) +(f6 IN (TRUE, "\t", "\n", 6.02e23)) AND (((f4 IN ('', 1, -10)) OR ((f6 <> 0.0000000001) AND (f6 != 1e10))) OR (f3 LIKE "a_b%")) +f6 NOT ILIKE "\n" +f2 BETWEEN -10 AND "xyz%" +f2 == "\n" +f4 IN (f2, "abc", 999999999999) +f3 >= f6 +f4 IN ('O''Reilly', -.5, 0) +hasAll(f3, -2.7) +(f6 BETWEEN 0.0000000001 AND f6) OR ((f1 IN (TRUE, TRUE, 'O''Reilly', f5, f4)) OR (f2 == 999999999999)) +f1 NOT +() +f5 IN (10, 'O''Reilly', -10, -3.5e-2, "\n") +hasToken(f1, f3) +hasAny(f2, 999999999999) +(f1 = -10 +f6 BETWEEN 1e10 AND 'O''Reilly' +f1 BETWEEN -2.7 +f3 BETWEEN 3.14 AND 3.14 +f1 NOT ILIKE 'test' +(f2 IN (-10)) OR (f1 BETWEEN -1e-10 AND 10) +() +f3 IN (TRUE, f1, "abc", 'O''Reilly') +f1 IN (1e10, -2.7, 6.02e23, TRUE, "hello world") +f4 < 3.14 +f6 BETWEEN AND -2.7 +NOT +f2 = +(f5 NOT LIKE "abc") AND (f5 IN (-.5, ' ')) +f5 BETWEEN f3 AND 10 +f2 BETWEEN 1 AND -1 +f6 LIKE +f1 IN ("%wild%", 1e10, -1) +has(f6, 6.02e23) +f5 NOT +f3 IN ("xyz%", f2, "\t", "%wild%") +hasAll(f1, 3.14) +(hasToken(f3, "\t")) OR (f1 BETWEEN -2.7 AND "\n") +f4 IN (0.0000000001, f6) +hasToken(f5, "\n") +(has(f2, 999999999999)) OR ((f6 ILIKE "hello world") AND (f1 <> "\n")) +f4 IN ("hello world", f2, 3.14, 0, 999999999999) +f3 BETWEEN .5 AND "xyz%" +f5 IS NULL +f6 IN () +f4 = +() +f5 BETWEEN -3.5e-2 AND 0 +NOT +hasAll(f6, f1) +f6 NOT CONTAINS '' +f3 NOT REGEXP '' +f6 IN ("a_b%", f4, "%wild%") +f3 BETWEEN -1e-10 AND 3.14 +f3 IN () +f5 IN (-3.5e-2, f1, f2, "a_b%") +f6 OR OR 0.0000000001 +f1 = +f1 BETWEEN AND 999999999999 +(hasAny(f4, 1e10)) OR (f2 BETWEEN "hello world" AND 999999999999) +(f4 BETWEEN "\n" AND TRUE) AND (hasAll(f6, f1)) +f1 = "hello world" +f2 BETWEEN 'O''Reilly' AND 0 +f2 IN ("abc", '', "\t", "\t") +hasAll(f4) +f1 CONTAINS ' ' +f6 IN (,) +f1 != ' ' +((has(f3, -3.5e-2)) AND (f3 IN ("abc", 1))) OR (f3 NOT REGEXP '') +f4 BETWEEN 1e10 AND TRUE +hasToken(f3, f4) +f3 IN (0, f2, 1, f4) +AND f1 = f6 +(f1 IN (-3.5e-2, "%wild%", 999999999999)) AND (f3 ILIKE '') +f2 BETWEEN f2 +f4 = '' +(f6 IN ('O''Reilly', 0.0000000001)) OR (hasToken(f4, -2.7)) +f5 ILIKE "hello world" +has(f3, f5) +(f1 IN (999999999999, '', f3)) OR (f1 = .5) +f6 IN (f3, f3, 0.0000000001) +f5 OR OR -.5 +hasToken(f3, "xyz%") +f3 IN ('O''Reilly', -2.7, "\t", "%wild%") +f3 OR OR 6.02e23 +(hasToken(f3, 'O''Reilly')) OR (f5 IN (TRUE)) +f5 BETWEEN "xyz%" AND .5 +f4 IS NULL +f5 NOT CONTAINS "\t" +f6 IN (f5, "abc", 'test', .5) +(has(f3, 999999999999)) OR (f3 IN (-1, .5, f3, 1)) +f1 LIKE "xyz%" +has(f6, -1) +NOT +(f6 ILIKE "xyz%") OR (hasToken(f2, f2)) +f3 BETWEEN -1e-10 AND f1 +f3 BETWEEN ' ' AND TRUE +f4 != -.5 +f3 BETWEEN 3.14 AND 999999999999 +f1 IN (0, -10) +f5 >= 'O''Reilly' +(hasToken(f2, "\t")) AND (f4 IN (TRUE, f2, -10, "hello world", 1)) +((f6 NOT ILIKE "a_b%") OR (f5 IN ("\n", 1))) AND (f2 IN ("abc", f4, "xyz%", -10)) +hasToken(f5, f1) +f1 BETWEEN -3.5e-2 +f4 IS NULL +f6 IN (,) +f1 = +f1 BETWEEN 999999999999 AND -2.7 +f4 BETWEEN f1 AND f6 +f5 < f5 +has(f3) +f1 == 1e10 +hasToken(f2, 0) +hasToken(f4, 'test') +f1 LIKE "%wild%" +f4 NOT LIKE '' +f4 ILIKE "\n" +f4 NOT CONTAINS "xyz%" +f1 BETWEEN f5 AND 6.02e23 +hasToken(f5, 10) +f5 <= 0.0000000001 +(f3 NOT REGEXP "%wild%") OR (hasToken(f4, 'O''Reilly')) +hasToken(f1, -1) +(has(f6, "\n")) OR (hasToken(f1, 0)) +f3 BETWEEN .5 AND f2 +f5 == '' +f2 <= 6.02e23 +f1 NOT CONTAINS 'O''Reilly' +f2 IN ("abc") +f6 ILIKE "a_b%" +f4 OR OR 1 +f5 = 'unclosed +has(f6, 0) +f2 NOT REGEXP "xyz%" +f3 IN (999999999999, ' ', .5, 1) +f6 IN [1 2 3] +hasToken(f6, 0) +f3 BETWEEN AND f3 +has(f4, 'O''Reilly') +f6 IN (0, "%wild%", 3.14, "\t") +f6 BETWEEN "a_b%" AND 999999999999 +f5 REGEXP "xyz%" +hasToken(f4) +(hasToken(f6, 0.0000000001)) AND ((f1 BETWEEN 0.0000000001 AND 0.0000000001) AND (f5 IN (f2, "%wild%", f5, "xyz%"))) +f3 LIKE +f4 IN ("%wild%") +f2 LIKE +f5 NOT CONTAINS "%wild%" +f3 REGEXP "\t" +f3 NOT CONTAINS "hello world" +f2 ILIKE "%wild%" +f3 BETWEEN f6 AND "%wild%" +(f5 BETWEEN 0 AND f6) OR ((hasToken(f3, 0)) OR (f6 ILIKE "\t")) +f1 NOT ILIKE '' +f5 NOT CONTAINS "abc" +f4 NOT CONTAINS ' ' +f1 NOT ILIKE "\n" +f2 CONTAINS 'test' +hasToken(f2, -10) +f1 >= "abc" +f1 = 'unclosed +f6 BETWEEN "abc" AND 10 +f2 IN [1 2 3] +() +f3 BETWEEN AND -.5 +f3 REGEXP '' +f5 NOT +f3 IN (0, "a_b%") +f4 REGEXP "hello world" +f4 <= f2 +f6 != -1 +hasAny(f2, .5) +f4 = 3.14 +(hasToken(f4, 999999999999)) AND (hasToken(f4, -3.5e-2)) +f5 === -10 +f4 <= 'O''Reilly' +f1 NOT ILIKE 'test' +f5 === f4 +f3 IN (6.02e23) +f2 IN () +hasAny(f4, -10) +f4 IN (-.5, f3, "a_b%", 1) +(f3 = 10 +hasAll(f3, -1e-10) +hasToken(f3, 6.02e23) +f6 IS NULL +f2 LIKE +f1 NOT LIKE "a_b%" +f5 <= f5 +f6 ILIKE "abc" +f1 IN (-.5, -1, .5, 6.02e23, ' ') +f6 BETWEEN 6.02e23 AND f2 +f3 LIKE +f5 BETWEEN f2 AND 3.14 +f1 BETWEEN f3 AND "%wild%" +f4 === 'O''Reilly' +f5 IN ("abc") +f3 BETWEEN f4 AND f2 +((f2 REGEXP 'O''Reilly') AND ((f6 BETWEEN "%wild%" AND 'O''Reilly') OR (hasAny(f4, "\n")))) OR (f1 NOT REGEXP "%wild%") +AND f3 = -1 +has(f4, 1) +has(f3, -10) +f4 IN (f4, TRUE) +f4 BETWEEN "\n" AND -.5 +f5 IN (-1, 0.0000000001) +has(f2, 3.14) +f6 IN (-2.7) +f4 BETWEEN "abc" AND -2.7 +f1 BETWEEN 'O''Reilly' +f1 == "hello world" +hasAny(f2, "%wild%") +(f3 ILIKE "hello world") AND ((hasToken(f4, f4)) OR (f3 <> 1)) +f2 BETWEEN AND f1 +f5 BETWEEN 1 AND -1 +(f3 NOT CONTAINS "%wild%") AND ((f2 IN (f6, f5, "abc", "a_b%")) OR (f6 IN ('test'))) +f1 BETWEEN "\t" AND f5 +(((f3 REGEXP "\n") AND (hasAll(f2, 0.0000000001))) OR (hasAll(f1, "a_b%"))) AND ((hasAll(f2, 1e10)) OR (hasAny(f6, "%wild%"))) +f5 BETWEEN -2.7 AND 0.0000000001 +f2 REGEXP "a_b%" +(f2 <> f1) OR (f2 != 'test') +f3 OR OR 10 +f4 = -.5 +f3 BETWEEN -2.7 AND 1 +f3 > "a_b%" +f1 REGEXP "\t" +f1 IN () +f3 BETWEEN '' AND 3.14 +f5 BETWEEN -1 AND f5 +f1 IN () +f1 NOT REGEXP ' ' +AND f3 = f5 +f4 = '') +f1 BETWEEN 0 AND f3 +f3 = 'test' +f5 IN ("\n", f6, ' ', TRUE, f2) +f3 <> 999999999999 +f1 LIKE "\n" +f1 < TRUE +f6 <> 6.02e23 +has(f5, 999999999999) +f6 NOT LIKE "%wild%" +f5 BETWEEN AND .5 +NOT +f2 NOT CONTAINS "hello world" +f6 = ' ') +f5 NOT ILIKE "abc" +() +f1 REGEXP "%wild%" +hasToken(f1, "\n") +(f3 NOT LIKE '') OR (f1 NOT LIKE "%wild%") +f1 NOT REGEXP "\t" +f4 IN ('O''Reilly', "abc") +f1 OR OR 1 +f4 IN (999999999999, "abc", f4) +f2 LIKE "\t" +f4 NOT CONTAINS "xyz%" +f4 BETWEEN -2.7 AND '' +f6 ILIKE "abc" +f2 BETWEEN AND "xyz%" +NOT +f3 BETWEEN "\n" AND -2.7 +f2 < "abc" +(f1 == 'test') AND (f2 IN ('O''Reilly', 0.0000000001, 1, f5, "hello world")) +hasToken(f5, "a_b%") +f4 === f6 +f4 BETWEEN "a_b%" AND "\t" +f2 BETWEEN "abc" AND -1 +f1 IN ("\n", f3, f2) +NOT +f1 = 'unclosed +f5 REGEXP 'O''Reilly' +hasToken(f1, -1e-10) +f2 IN ("\t", "a_b%", 'test', 'O''Reilly') +f2 != "xyz%" +f3 BETWEEN "\t" AND -1e-10 +f4 BETWEEN "abc" AND f3 +f1 IN (-10, .5) +f1 = +f3 >= 3.14 +f5 = f5) +(f1 IN (0.0000000001)) OR (f3 == -1e-10) +f5 === -2.7 +f4 = 'O''Reilly' +f5 > TRUE +(f4 >= -2.7) OR ((f5 NOT ILIKE "xyz%") AND (f1 BETWEEN "xyz%" AND ' ')) +f4 NOT +f4 = +f2 BETWEEN -2.7 AND 0.0000000001 +has(f3, 1e10) +f4 IS NULL +f6 = ' ' +f2 NOT LIKE "\t" +hasAll(f1) +f3 LIKE "abc" +f5 IN (f3, 'O''Reilly', "\n", "hello world") +hasToken(f1, 0) +f2 IS NULL +f6 BETWEEN f4 AND "a_b%" +f5 IN (-1e-10, -1) +f1 == f2 +(f1 < 1) AND (f3 < 'test') +f3 IN (f1, f2, "hello world", 0.0000000001, 0.0000000001) +(f6 IN (TRUE, "%wild%", '', -3.5e-2, 0.0000000001)) AND (f6 LIKE 'test') +f2 IN (10) +(hasToken(f2, "\t")) OR (f6 BETWEEN "\n" AND .5) +AND f5 = f3 +f6 ILIKE 'test' +f5 IN () +f2 < -1e-10 +f4 = NULL +(f3 = "%wild%" +hasAny(f5, '') +f4 OR OR -2.7 +f5 BETWEEN "abc" AND "xyz%" +f6 NOT LIKE '' +(f4 = 1 +f1 = -10) +hasAny(f4, "xyz%") +AND f6 = 3.14 +f5 BETWEEN 'O''Reilly' AND "\n" +f5 NOT +hasToken(f1, f3) +NOT +f1 BETWEEN TRUE AND 999999999999 +AND f5 = 'test' +f1 IN ("abc", f1, f6, -1e-10) +(hasToken(f1, f3)) OR (f1 IN (6.02e23, 0.0000000001)) +f6 LIKE +hasToken(f2, "xyz%") +hasAll(f5, -3.5e-2) +() +f1 == 'O''Reilly' +f2 = NULL +f3 IN (10, "\n", -3.5e-2) +(f5 = 1 +AND f4 = .5 +has(f3, 0) +f3 IN (f5, f3, 0.0000000001, f3, -1e-10) +f4 IN ("a_b%", ' ', -10, 6.02e23, "xyz%") +f6 IN (-10) +(f4 LIKE 'test') OR (f3 BETWEEN "\t" AND f3) +f1 = +f4 IN (-1, f6) +hasAll(f2, -.5) +f2 BETWEEN .5 AND -1 +() +f1 BETWEEN f4 AND -.5 +has(f4, 'test') +f3 NOT LIKE "%wild%" +f4 BETWEEN f1 AND 999999999999 +f1 IN ("a_b%", ' ') +(f1 > 3.14) AND (hasAll(f1, "xyz%")) +f4 BETWEEN 1e10 AND 999999999999 +f2 <= 0 +f2 NOT +(f4 BETWEEN "%wild%" AND 999999999999) AND (f5 ILIKE '') +hasToken(f1, "\t") +f4 CONTAINS '' +(f2 <> 0) OR (f3 IN (-10, 1, "\n")) +f3 NOT CONTAINS "abc" +hasAll(f3, "%wild%") +f3 == f5 +f5 BETWEEN TRUE AND -3.5e-2 +hasAll(f2, -2.7) +f3 = TRUE +(f6 <> .5) OR (f6 BETWEEN "%wild%" AND '') +f6 BETWEEN -3.5e-2 AND -.5 +hasAll(f1, f6) +f2 = NULL +f2 === '' +hasToken(f3) +f3 LIKE 'test' +f5 IN (0, f4, .5, f1, -10) +f3 BETWEEN AND -.5 +f5 IN (f2, "a_b%", f2, f5) +f1 <> f2 +f5 NOT +(f4 = ' ') OR (f1 > -3.5e-2) +f2 BETWEEN "\n" AND "\n" +hasAll(f2, f3) +(f6 BETWEEN f6 AND 0.0000000001) AND ((f4 IN (.5)) AND ((hasToken(f6, -.5)) OR (f6 > "\t"))) +NOT +hasAny(f3, f3) +f1 BETWEEN 0 AND 0 +f5 REGEXP "\n" +hasAll(f6, "hello world") +((f3 IN (-1)) OR (f5 ILIKE "%wild%")) AND (f6 NOT REGEXP 'O''Reilly') +f4 IN (-2.7, "\t", 1) +f6 OR OR 10 +f4 IN (0.0000000001, f5, "%wild%", TRUE, -10) +hasAny(f3, 'O''Reilly') +f6 NOT ILIKE "abc" +f6 = +hasToken(f2, -3.5e-2) +f6 BETWEEN f6 AND "a_b%" +f2 BETWEEN TRUE AND "%wild%" +has(f3, 1e10) +f6 BETWEEN "\t" AND "a_b%" +f2 NOT REGEXP "\t" +f3 >= 6.02e23 +(f2 BETWEEN -.5 AND "abc") OR (f4 NOT LIKE 'O''Reilly') +has(f3, "abc") +f3 IN (f2, -1, 6.02e23) +f1 NOT CONTAINS 'O''Reilly' +f3 REGEXP "a_b%" +f4 NOT LIKE "hello world" +has(f2, 6.02e23) +(f3 LIKE ' ') OR (f2 != 6.02e23) +hasToken(f5) +f3 IN ("a_b%", "%wild%", "%wild%") +f2 NOT REGEXP "%wild%" +f4 REGEXP 'test' +f4 BETWEEN TRUE AND -1e-10 +hasAll(f1, TRUE) +hasAny(f2, "\t") +(f4 BETWEEN 1 AND 1e10) AND (f1 BETWEEN f2 AND '') +f6 IN () +f6 BETWEEN AND -2.7 +hasAny(f4, .5) +(hasAny(f6, "abc")) AND (has(f6, -3.5e-2)) +f6 ILIKE "xyz%" +f2 IN (f4) +f4 BETWEEN -1 AND 10 +f2 BETWEEN AND 6.02e23 +hasAll(f5, f5) +hasAny(f4, 6.02e23) +f2 BETWEEN -3.5e-2 AND 1 +f2 IN [1 2 3] +(f4 LIKE "\n") OR (((f4 IN ("xyz%", "%wild%")) AND (f4 IN (f2, f3))) OR (f4 REGEXP "abc")) +(hasToken(f3, "a_b%")) OR (f4 LIKE "%wild%") +hasToken(f5, 6.02e23) +f4 BETWEEN 10 AND -.5 +f4 BETWEEN -10 AND "\n" +f1 LIKE "a_b%" +f1 CONTAINS "abc" +(f5 LIKE 'test') AND ((f4 REGEXP ' ') OR ((f4 BETWEEN -1e-10 AND .5) AND (f2 IN ('', "abc", '', "hello world", 3.14)))) +f1 = +(f4 NOT LIKE "a_b%") AND (f4 >= -2.7) +f4 < 10 +f4 BETWEEN '' AND f2 +f6 ILIKE "\t" +f4 = NULL +f4 > 1 +f3 = 6.02e23 +f5 IN (-3.5e-2, 0.0000000001, 1e10, -1) +(hasAll(f4, f4)) OR ((((f3 <> f1) OR (f5 >= 'O''Reilly')) OR (f3 BETWEEN 6.02e23 AND "\n")) OR ((f1 BETWEEN 'test' AND 3.14) OR (f1 IN (999999999999, f2, 'test')))) +(f2 <> f3) AND (f4 CONTAINS "\n") +f6 IN () +f4 NOT +f5 = 999999999999 +f3 NOT +hasToken(f5, 999999999999) +f2 IN (6.02e23, f2, -2.7) +f1 = '' +f1 OR OR -3.5e-2 +f4 BETWEEN AND .5 +f2 BETWEEN f5 +f5 BETWEEN f4 AND -2.7 +f1 BETWEEN 'test' AND f4 +f3 IS NULL +f4 === 1e10 +hasAll(f1, ' ') +f5 BETWEEN f6 +f5 IN (f2, 'test', -10) +f1 != 999999999999 +f6 > f1 +f3 NOT LIKE "\n" +f2 LIKE "xyz%" +f5 IN ('test', f4, "abc") +(f1 NOT LIKE "a_b%") OR (hasAny(f4, -.5)) +f1 BETWEEN f3 AND 1 +f5 IN (1, "a_b%", 0.0000000001) +f2 != f2 +f2 IN (10, 999999999999) +f6 OR OR 'test' +f1 != -10 +(f5 ILIKE '') OR (hasToken(f1, TRUE)) +(f5 IN ("%wild%", f6, "\t", "\n", 0.0000000001)) OR (f3 REGEXP "xyz%") +f2 != "%wild%" +f6 BETWEEN "\n" AND f1 +f5 IN ("\n", 'O''Reilly', 1e10) +f6 BETWEEN 'test' AND f2 +f3 === "abc" +f1 BETWEEN 999999999999 AND f1 +() +f2 IN (6.02e23, f3, "\n", 1) +f1 BETWEEN f6 AND f4 +f3 BETWEEN f2 AND 3.14 +hasAny(f1) +f2 NOT ILIKE "\t" +f4 LIKE "abc" +f3 BETWEEN ' ' AND f4 +f4 BETWEEN 0.0000000001 AND -.5 +f4 IN ('O''Reilly') +f1 IN ("%wild%", "%wild%") +f5 NOT ILIKE "\n" +f4 IN [1 2 3] +f4 IN (0.0000000001, "\t", "%wild%", "\n") +f3 IN ("xyz%", f1, f5, 0.0000000001, 999999999999) +f5 BETWEEN -10 AND -3.5e-2 +hasAll(f3, f3) +f4 === f1 +f4 <= 1 +hasAny(f3, 10) +(f2 BETWEEN f6 AND 1) AND (f6 BETWEEN f6 AND f5) +f2 NOT ILIKE 'test' +has(f5, "a_b%") +f5 NOT +NOT +f1 = NULL +f5 IN ('', '', 1, "%wild%", "a_b%") +f2 IN (,) +f5 IS NULL +f5 IN () +f4 IN ('test', "a_b%", 6.02e23) +f1 BETWEEN -.5 AND f4 +hasToken(f3, -1e-10) +(f3 BETWEEN 10 AND -1e-10) OR ((f6 BETWEEN 3.14 AND -3.5e-2) OR (f1 IN (-1e-10, f1))) +(has(f3, 1e10)) AND (hasAll(f1, -3.5e-2)) +has(f3, 1) +f5 REGEXP "xyz%" +(f3 BETWEEN 'O''Reilly' AND TRUE) AND (f5 == .5) +f6 IN ("xyz%", f6) +f1 NOT ILIKE 'test' +(f6 ILIKE ' ') OR (has(f4, 1e10)) +f2 IN (.5) +f5 BETWEEN 3.14 AND 999999999999 +f1 BETWEEN 3.14 AND 'test' +f5 IN (-3.5e-2) +has(f1, "\n") +f3 IN (-1e-10, .5) +() +(f4 != 0.0000000001) OR ((f1 REGEXP "xyz%") OR (f1 <> 3.14)) +hasAll(f4, -1) +f1 <> 999999999999 +f1 NOT +f4 IS NULL +f4 <> .5 +f6 BETWEEN f5 AND f6 +f6 = "a_b%" +f6 IS NULL +f5 IN ("hello world", "\t", "\t", "\t", "abc") +NOT +f1 = 'unclosed +f4 > f5 +((hasAny(f2, "abc")) OR (f5 IN (f5))) AND (f5 BETWEEN "a_b%" AND 6.02e23) +hasAll(f3, ' ') +f2 == TRUE +(f2 = f5 +f6 NOT +hasToken(f3, TRUE) +AND f3 = f4 +f1 BETWEEN f3 AND "%wild%" +f5 BETWEEN f2 AND f4 +f3 > f6 +has(f1, "hello world") +f3 >= 1e10 +(((f1 <= "\n") AND (f3 <= 1)) AND (f1 NOT LIKE '')) AND (f6 LIKE "a_b%") +(f4 IN ("xyz%", 1e10, "\t")) AND (f5 REGEXP 'test') +f3 = -1e-10 +((f3 IN (' ', TRUE, f1)) AND (f2 = 3.14)) AND (hasAny(f4, 6.02e23)) +f2 BETWEEN "\n" AND f4 +f6 IN [1 2 3] +(f1 NOT ILIKE 'O''Reilly') AND ((hasToken(f1, '')) AND (has(f1, -1e-10))) +(f2 < "xyz%") OR (f6 BETWEEN f4 AND f3) +(f4 BETWEEN "%wild%" AND "%wild%") AND (f2 IN (' ', "xyz%", 'test', "\n", f1)) +f2 IN (f4) +f6 = +(f6 >= 999999999999) OR (f4 IN ('test', "a_b%", 1e10, 0.0000000001, "a_b%")) +f6 NOT ILIKE '' +f4 BETWEEN "hello world" AND f6 +((f1 == f2) AND (f5 NOT LIKE "hello world")) OR ((f4 IN (-10, "abc", -1, "hello world")) OR (has(f3, 'test'))) +(f3 BETWEEN ' ' AND 6.02e23) AND ((f2 NOT CONTAINS "a_b%") OR ((has(f5, 999999999999)) AND (f4 BETWEEN "a_b%" AND "\n"))) +f6 NOT CONTAINS "xyz%" +f3 IN [1 2 3] +f1 NOT LIKE 'O''Reilly' +f3 OR OR "abc" +f2 IN (,) +(f1 LIKE ' ') AND ((f2 IN ("%wild%", -2.7)) AND (f6 = -3.5e-2)) +f4 OR OR 1 +f2 LIKE +f1 = NULL +f5 <= f1 +NOT +f6 BETWEEN f1 AND f5 +hasToken(f4, 1e10) +f1 IN ('test', "hello world", "%wild%", "\t") +f4 BETWEEN f4 AND "\t" +f1 IN (f6, -3.5e-2, f6, -2.7, f2) +AND f3 = f4 +(f1 = 999999999999 +f3 IN ('', f2) +f5 = f3 +AND f3 = "\n" +f4 IN (f4, '', .5, -.5) +hasToken(f2, 1) +f2 IN (TRUE, 999999999999, -1e-10, -.5) +f2 BETWEEN "xyz%" AND -.5 +f6 IN (-3.5e-2, 0.0000000001, f3, -.5) +hasAll(f2) +((f5 < 6.02e23) AND (f1 IN (f2, 0))) OR (f2 BETWEEN 0.0000000001 AND -1e-10) +(hasAny(f5, "hello world")) OR (f6 NOT LIKE "hello world") +f5 LIKE 'O''Reilly' +f1 NOT REGEXP "\t" +f4 CONTAINS '' +(f3 NOT REGEXP "a_b%") OR (f6 IN (-2.7, '')) +(f4 BETWEEN -3.5e-2 AND f1) OR (f3 <> 6.02e23) +f2 CONTAINS '' +hasAll(f2, -.5) +has(f2, 0.0000000001) +hasToken(f5, 999999999999) +f6 > 1 +f5 > TRUE +NOT +(f6 NOT CONTAINS 'test') OR (f4 NOT CONTAINS "xyz%") +f5 === 6.02e23 +f4 ILIKE "hello world" +f5 IN ("abc", f2, 'test', 999999999999) +f2 REGEXP "\n" +has(f3, -1e-10) +f3 IN (TRUE, '', f1) +f6 BETWEEN AND '' +(f6 != f3) AND (f6 CONTAINS 'test') +f3 BETWEEN "\n" AND f3 +(f6 BETWEEN 999999999999 AND 'test') OR (f5 IN (-10, f1)) +f1 == -1 +f5 IN (f1, "\t", 1, f2, 999999999999) +f6 OR OR -1e-10 +(f4 <= 10) AND (hasToken(f2, -2.7)) +f3 = "hello world") +hasAll(f3, f5) +(f6 = 1 +f5 <> f2 +f6 IN (f2, 3.14, f6, "abc") +NOT +f6 BETWEEN "a_b%" AND 10 +f5 <= "hello world" +hasAny(f6, "xyz%") +f3 LIKE +f1 NOT ILIKE '' +f5 = 999999999999 +f4 IN ('test', "\n", f1, 0, 'O''Reilly') +f2 >= "\t" +f4 IN (.5, 3.14, f1, 3.14) +f3 = "abc" +((f5 IN (f6, -10, -1, -1)) OR (f3 <> 999999999999)) AND (f2 IN (TRUE, "\t", 0, -2.7, 0.0000000001)) +f6 IN (1) +f4 IN (0, -2.7) +() +(f5 < .5) AND (f6 NOT REGEXP "abc") +f3 = NULL +f5 = 'O''Reilly' +f5 BETWEEN 999999999999 AND -3.5e-2 +f5 = 'unclosed +f1 BETWEEN AND -1e-10 +f2 IS NULL +f4 <= -2.7 +f4 BETWEEN -.5 AND -1 +f4 > -10 +f6 NOT LIKE "hello world" +has(f4, -10) +f6 != 0.0000000001 +f5 IN [1 2 3] +f1 BETWEEN f1 AND 6.02e23 +f6 IN (f1, f6, -2.7) +f5 === "\t" +f1 BETWEEN 999999999999 AND f1 +f4 < -2.7 +f5 NOT +hasAll(f2, f1) +f3 IN (' ', 1, "\t", 0, "%wild%") +() +hasAll(f6, -1) +f5 BETWEEN "hello world" AND 3.14 +((f5 IN (f3, f2, f1)) AND (f2 IN ('test', f3, "\t", "\n", 3.14))) OR (f1 BETWEEN -2.7 AND "\t") +f3 IN [1 2 3] +AND f2 = -.5 +() +(f4 LIKE "xyz%") OR (f4 IN (f5, f5)) +AND f3 = -3.5e-2 +f2 IN ("%wild%", 6.02e23) +(f1 > 'O''Reilly') OR (f4 != -2.7) +f3 != 0 +(f2 IN (-1e-10)) AND (hasAll(f3, TRUE)) +hasAny(f5, 'test') +f2 BETWEEN -.5 AND .5 +f2 < "xyz%" +f2 NOT +f6 BETWEEN f3 AND f4 +hasAll(f2, -1) +NOT +f1 <= 6.02e23 +f2 = NULL +f3 IN (TRUE, 999999999999) +f3 === -1e-10 +f2 IN (.5, -10, f3) +has(f6) +hasToken(f2, -3.5e-2) +f4 IN (f4, f1, "\n", "\n") +hasToken(f2, 'O''Reilly') +(f1 ILIKE "abc") OR (f2 IN (999999999999)) +f5 NOT REGEXP "\n" +f6 IN ("\t", -.5) +(f3 NOT LIKE 'O''Reilly') OR (f1 BETWEEN .5 AND 0) +f1 IN (,) +f2 BETWEEN -10 AND f4 +f6 > 0.0000000001 +f3 BETWEEN f2 AND -3.5e-2 +has(f4, 3.14) +f6 <> 'O''Reilly' +f5 IN (f5, '', ' ', TRUE, -10) +has(f5, f4) +f6 != .5 +hasToken(f2, "xyz%") +f2 ILIKE ' ' +f5 BETWEEN -3.5e-2 +f4 = 'test' +(f6 IN (999999999999, "\t", f3, 'O''Reilly', f3)) OR ((f1 IN (1, 0.0000000001, "\t", "\n")) OR (f4 IN (0))) +f4 ILIKE "hello world" +() +hasToken(f3, -.5) +(hasAny(f2, 0)) OR (((f5 ILIKE "\t") AND (f4 CONTAINS "abc")) OR (hasAny(f3, -2.7))) +f6 IN (999999999999, "%wild%", "hello world") +has(f1, 'O''Reilly') +hasAll(f4) +(f6 LIKE 'O''Reilly') AND (f3 NOT LIKE "xyz%") +(hasAll(f3, '')) OR (f1 BETWEEN "abc" AND f5) +f1 BETWEEN 999999999999 AND TRUE +(f5 IN (1e10, f5, "\t", f3)) OR (hasToken(f4, f5)) +f3 BETWEEN f1 AND -10 +f1 IN ("xyz%", -10, ' ', 0.0000000001) +f5 = +f4 NOT LIKE "a_b%" +f1 IN (,) +(f3 NOT CONTAINS "\n") AND (f3 = 3.14) +(f1 CONTAINS 'O''Reilly') OR ((f3 == "%wild%") OR (f5 BETWEEN 1 AND "\n")) +hasToken(f4, 0.0000000001) +f1 BETWEEN "\t" AND .5 +f2 BETWEEN f3 AND 1e10 +f6 BETWEEN 1e10 AND 10 +f5 NOT CONTAINS "hello world" +f3 IS NULL +f6 > 'O''Reilly' +f4 >= 999999999999 +f6 BETWEEN TRUE AND 0 +NOT +hasAny(f4, ' ') +f5 REGEXP 'test' +AND f2 = "hello world" +f6 CONTAINS "a_b%" +f6 IN (f2, "\n", "\n") +hasAll(f6, f2) +f3 IN ("hello world", f2) +f3 === "xyz%" +f5 < "\t" +f6 BETWEEN "xyz%" AND "%wild%" +() +(f4 IN (-1e-10, 1e10, .5, "abc", TRUE)) OR (f6 IN (-1, "hello world", 1, f4, 0.0000000001)) +f3 == ' ' +f2 ILIKE 'O''Reilly' +f3 = 'unclosed +f4 IN ("hello world", f1, "hello world", "\t") +f6 LIKE +f1 BETWEEN 'test' AND -10 +f1 != f5 +f5 NOT LIKE "\t" +NOT +f1 BETWEEN f1 AND 3.14 +f3 BETWEEN f2 AND '' +f5 NOT ILIKE "a_b%" +f6 IN (TRUE, -1) +f1 CONTAINS '' +f4 LIKE ' ' +((f5 BETWEEN 1e10 AND "abc") OR (f2 == -.5)) AND (f4 REGEXP "a_b%") +hasAll(f5, -2.7) +f2 BETWEEN f5 AND TRUE +f4 OR OR -10 +f4 < -1 +f5 NOT ILIKE '' +f4 IN ("hello world", f6, -1e-10, 0, 0.0000000001) +f4 IN (f1, "%wild%") +(f3 < -.5) OR (f5 BETWEEN 10 AND 0) +f5 NOT ILIKE "\t" +f3 NOT +f5 = '') +(f4 BETWEEN 10 AND .5) OR (f2 BETWEEN -1 AND -.5) +hasToken(f6, "\n") +f5 != "abc" +f3 REGEXP "abc" +f1 === ' ' +f6 BETWEEN ' ' AND 999999999999 +f1 IN ('test') +f6 != -1 +f6 OR OR -1 +f6 IN (999999999999, ' ', f2, f5) +f5 BETWEEN 6.02e23 AND "hello world" +f3 = 'unclosed +f2 OR OR f6 +f6 CONTAINS '' +f6 IN [1 2 3] +(f3 IN (f6, f3, "\n")) AND ((f6 < f6) OR (has(f6, "a_b%"))) +hasAny(f5, "xyz%") +(f2 > 1) OR (f5 NOT REGEXP "a_b%") +f5 IS NULL +f3 IN (f2, f5, f5, "hello world") +f3 BETWEEN 'test' AND .5 +hasAny(f4, 'test') +f4 BETWEEN f4 AND -1 +f6 < "%wild%" +AND f5 = 'O''Reilly' +hasToken(f3, f5) +f3 IN (0.0000000001, "%wild%", 1, -3.5e-2, f2) +f5 IN ("\n") +f2 BETWEEN f5 AND 6.02e23 +f3 NOT ILIKE "xyz%" +f3 IN (,) +f3 LIKE +hasAny(f6, 999999999999) +((f5 LIKE '') OR (f3 == 1)) OR ((f2 IN (f4)) AND (f3 IN ("a_b%", -3.5e-2, 3.14, f3))) +f4 BETWEEN "\n" AND -1 +f5 BETWEEN AND "abc" +has(f2, 3.14) +f2 BETWEEN -1 AND -3.5e-2 +f1 <> "abc" +hasAny(f4, f5) +f1 BETWEEN "abc" AND f2 +(hasAny(f5, 'O''Reilly')) OR (f5 NOT LIKE "%wild%") +f4 IN () +f6 BETWEEN "\t" AND f1 +f2 IN ('O''Reilly', "hello world", 1, "xyz%") +hasAny(f2, f5) +f4 NOT ILIKE ' ' +(hasToken(f1, f2)) OR (f5 IN (-3.5e-2, 'O''Reilly', "\n", f2, 3.14)) +f4 IN (999999999999, "%wild%", "abc", 3.14, "a_b%") +f4 REGEXP "\t" +has(f3, 6.02e23) +hasToken(f1, "hello world") +has(f5) +((f4 IN (f5, -.5, 1e10, 10, 0.0000000001)) OR (f2 BETWEEN f5 AND "hello world")) AND (f6 NOT LIKE 'test') +f3 NOT REGEXP ' ' +f3 > "a_b%" +((((f3 >= f6) OR (f5 = 10)) OR (f1 IN ("\t", f4))) OR (f6 IN ("xyz%", 999999999999, TRUE))) AND (f4 BETWEEN -1 AND '') +f4 IN (-1e-10, "xyz%", "xyz%") +f3 != 3.14 +hasToken(f5) +(f5 > 1) OR (f3 NOT LIKE 'test') +has(f3, 3.14) +f2 BETWEEN 1e10 AND f6 +f4 = 'unclosed +f1 <= -1 +f6 IN () +f4 IN (TRUE, '', 1) +f4 BETWEEN 1 AND 1e10 +has(f4, .5) +f2 OR OR 0 +f4 == 0 +f2 BETWEEN -2.7 AND "\t" +f3 NOT REGEXP 'test' +AND f1 = f2 +f1 BETWEEN 1e10 AND TRUE +f4 IN (1e10) +f1 CONTAINS "a_b%" +f4 != "xyz%" +f3 IN (TRUE, f3) +f1 = +f6 IN (f5, -1, f1, 0) +f1 != 3.14 +f4 >= -1e-10 +f2 IN [1 2 3] +f4 > f4 +f4 === 6.02e23 +f4 IN ("%wild%", "%wild%", .5, 1, 6.02e23) +f1 IN ("a_b%") +has(f5, "a_b%") +hasAny(f2, 'test') +f6 IN (0.0000000001, TRUE) +f1 BETWEEN "a_b%" AND f4 +f6 IN (-1e-10, f3, 10) +has(f3, "\t") +f3 NOT +f2 > -3.5e-2 +(f6 BETWEEN f4 AND 3.14) OR ((f4 IN (-2.7, f3, "\n", f2, '')) AND (f5 IN (f5))) +() +(f4 <> "\n") OR ((f5 < 'O''Reilly') AND (f3 REGEXP "a_b%")) +f6 = +f2 LIKE ' ' +(f2 ILIKE ' ') AND (hasAny(f6, TRUE)) +f6 BETWEEN "\n" AND -10 +f5 LIKE "xyz%" +hasToken(f2, 0) +f5 < 0 +NOT +((f4 NOT CONTAINS ' ') AND (f3 >= -.5)) OR (has(f2, -2.7)) +f6 CONTAINS 'O''Reilly' +f4 NOT ILIKE "\t" +hasToken(f3, .5) +f2 IN [1 2 3] +f6 LIKE +((f6 NOT CONTAINS 'test') OR (hasAny(f1, .5))) OR ((f5 IN ('test', f5, f2, "abc")) OR (hasAll(f5, 'O''Reilly'))) +f3 BETWEEN "\n" AND f4 +f2 = "xyz%") +f2 <= f5 +f5 <= -1e-10 +f6 BETWEEN -1 AND 999999999999 +f5 BETWEEN f4 AND 999999999999 +(hasToken(f2, '')) AND (f3 BETWEEN "%wild%" AND 1e10) +f1 BETWEEN -.5 AND "hello world" +f4 IN [1 2 3] +AND f5 = 'test' +f4 >= f6 +f2 < f2 +(f3 > f4) OR ((hasToken(f1, 'O''Reilly')) AND (f2 < 999999999999)) +f2 IN ("abc", f6, -10) +f4 IN (-1, 6.02e23, f3) +has(f3, -1e-10) +(f6 NOT REGEXP "%wild%") AND (hasAny(f3, -1e-10)) +f5 CONTAINS ' ' +f5 LIKE +(f3 != f5) OR (f5 IN (' ', "hello world")) +AND f2 = 0 +f6 LIKE +(f2 < f3) OR (f1 NOT LIKE "%wild%") +(f2 REGEXP "a_b%") OR (f4 IN (0, 1e10)) +f2 = NULL +f1 BETWEEN "hello world" +f3 IS NULL +f1 IN (-1e-10, "%wild%", -.5, -10) +((f2 != 'test') AND (f3 CONTAINS "xyz%")) OR (hasAll(f1, f1)) +f5 > "hello world" +f6 BETWEEN -1e-10 AND f1 +(f5 BETWEEN "%wild%" AND -10) OR (f6 NOT REGEXP '') +(f6 > f4) AND (hasAll(f1, 1e10)) +f5 IN (-3.5e-2, 3.14) +f1 IN (,) +f2 > "\t" +f5 LIKE +f6 IN (,) +f5 CONTAINS "%wild%" +f5 IN (6.02e23) +hasAny(f1, "\n") +(hasToken(f1, 'O''Reilly')) OR ((hasAll(f1, -1e-10)) AND ((has(f6, -2.7)) AND (f1 BETWEEN "a_b%" AND -.5))) +f4 IN (-3.5e-2, "abc", 'test', 1e10, "xyz%") +hasToken(f1) +f2 IS NULL +(f4 = f5 +f3 === f5 +f6 = NULL +f2 IN (,) +() +hasAny(f2, "\n") +(f4 BETWEEN f1 AND 1) OR ((f2 IN (' ', f3)) AND (f5 > "abc")) +f6 <= 1e10 +(f2 IN (f3, 10, ' ')) AND (f6 LIKE ' ') +f6 < 'test' +hasToken(f5, -1e-10) +f4 BETWEEN -.5 +f3 IN [1 2 3] +f2 <> -1 +hasAll(f1, 'test') +f2 NOT REGEXP "xyz%" +f6 = 0.0000000001) +f6 IN (0, "%wild%", 1, "xyz%", -10) +f2 ILIKE 'test' +f6 NOT CONTAINS '' +f4 IN () +f5 === 'O''Reilly' +f5 IN ("abc", f5, 999999999999, -10) +hasAny(f2, -2.7) +f5 = +has(f1, "abc") +f5 IN (3.14, -1, -1) +hasAll(f2, -10) +f5 == f2 +hasToken(f3, -10) +hasToken(f4, -1e-10) +f3 >= 0 +f1 BETWEEN "hello world" AND -1e-10 +(f2 IN ('O''Reilly', 'O''Reilly')) AND (f1 <= '') +f1 < "\t" +f6 < .5 +f3 BETWEEN '' AND "%wild%" +f4 = ' ') +f6 = NULL +(f1 IN (f6, f4)) OR ((f2 BETWEEN 3.14 AND 0.0000000001) OR ((f1 BETWEEN "hello world" AND f6) OR (f1 BETWEEN "hello world" AND ''))) +f3 IN (f6, TRUE, f6) +f1 BETWEEN AND f3 +(f4 IN (f6, "%wild%", -1e-10)) AND (hasAll(f6, "abc")) +(f3 < 'O''Reilly') AND (f5 BETWEEN 1e10 AND f2) +hasAll(f3, 1) +f2 OR OR 10 +hasAll(f6, 1e10) +f3 IN ("%wild%", 0.0000000001, "a_b%") +f4 <= f3 +hasAny(f5, -.5) +f1 = NULL +() +f1 IN ('O''Reilly') +f4 NOT LIKE '' +f2 IN (-.5, 'test', 'O''Reilly', f5) +f5 IN (TRUE, f4, 3.14, .5, 1) +f5 BETWEEN AND f3 +(f1 BETWEEN f3 AND "abc") AND (f4 IN (f4, "xyz%", 10, 999999999999)) +f3 <> f6 +f4 = +has(f1, -1e-10) +hasAll(f4, "abc") +f3 > "\n" +f1 = +f4 BETWEEN "hello world" AND -1e-10 +f3 BETWEEN "\n" AND f4 +(f4 BETWEEN -.5 AND "a_b%") AND ((f2 BETWEEN .5 AND -10) OR (f1 IN (.5))) +f6 != 1 +f1 NOT ILIKE "%wild%" +f1 BETWEEN -1 AND '' +f2 = 'unclosed +(f4 IN (.5)) OR (f6 > 'O''Reilly') +f4 BETWEEN 3.14 AND "hello world" +f3 BETWEEN AND "a_b%" +f5 IN ('O''Reilly', -1, 1e10, -1e-10, -.5) +hasAny(f3, ' ') +((((f5 = "%wild%") AND (f4 > -1)) OR ((f5 == f2) AND (f3 < 'test'))) AND (f2 IN (f4, f5, f5))) OR (f3 IN ("\n", "xyz%", "abc")) +f3 LIKE "abc" +f5 ILIKE "%wild%" +f2 NOT CONTAINS "a_b%" +AND f2 = f3 +f1 IN (-3.5e-2, '', "xyz%", ' ') +has(f4, 3.14) +AND f1 = 6.02e23 +f6 >= 'test' +(f2 != '') AND ((((f3 <> f6) OR (f3 != f3)) OR (f6 BETWEEN "abc" AND f1)) AND (f5 REGEXP "\n")) +f3 IN (' ', ' ', f3) +f2 >= -1e-10 +f6 REGEXP "\t" +(f6 NOT LIKE '') AND (f2 BETWEEN "hello world" AND 1) +hasToken(f2, -.5) +f4 <= "hello world" +(f3 <= "%wild%") OR (f1 != "\n") +f1 = ' ' +f6 BETWEEN -.5 AND -3.5e-2 +((f5 IN (-3.5e-2, -.5, f2)) OR (f4 REGEXP "hello world")) AND (hasAll(f2, "xyz%")) +f6 BETWEEN f3 AND -1e-10 +f6 BETWEEN f2 AND 999999999999 +(f1 BETWEEN ' ' AND .5) AND (f2 <= 0) +f1 BETWEEN "xyz%" AND -1e-10 +f6 OR OR f5 +f1 > 1e10 +hasAny(f5, -10) +f1 BETWEEN '' AND 10 +f2 BETWEEN f3 AND f2 +f2 BETWEEN AND -2.7 +f2 = NULL +f5 >= 3.14 +hasAny(f1) +f5 IN (f3, ' ', .5, f3, '') +hasAny(f1, -3.5e-2) +has(f5, f1) +hasAll(f5) +() +f5 > -2.7 +f3 >= 1e10 +f4 BETWEEN f2 +f3 != -1 +f6 BETWEEN "hello world" AND "a_b%" +f6 IN (,) +f5 BETWEEN 'test' AND -2.7 +(f3 LIKE ' ') OR (hasAll(f5, "a_b%")) +has(f1, 10) +f4 ILIKE "%wild%" +f5 >= "abc" +f5 NOT REGEXP 'test' +f1 != "hello world" +f4 < ' ' +hasAny(f5, "xyz%") +(f4 = f4 +hasAll(f4, "\t") +f2 = NULL +f4 IN (1e10, -10, 6.02e23, "abc") +hasToken(f3, f6) +(f3 = "%wild%" +has(f5, "%wild%") +hasAny(f5) +f4 = +f4 = 1 +f1 REGEXP 'test' +f1 LIKE 'O''Reilly' +f6 >= 'O''Reilly' +f3 = f3) +f3 >= f5 +() +f1 IN (-3.5e-2, f5, f6) +f6 BETWEEN -10 AND "xyz%" +(hasToken(f5, 1e10)) OR (f6 BETWEEN "xyz%" AND 1e10) +f2 BETWEEN 999999999999 AND 0.0000000001 +hasAny(f5, .5) +f1 >= .5 +f6 BETWEEN 0 AND 'test' +hasAll(f3, TRUE) +f1 NOT REGEXP "\t" +((f4 LIKE 'O''Reilly') OR (f5 NOT ILIKE "\n")) AND (f4 < '') +f4 NOT CONTAINS "%wild%" +AND f4 = "\n" +(f6 IN (f1, 'O''Reilly')) OR (f2 IN (f5)) +f5 IN (f5, -.5, f6, f5, 6.02e23) +f6 IN (6.02e23, -3.5e-2, 0) +(has(f3, TRUE)) AND (hasToken(f5, 0.0000000001)) +f6 REGEXP "abc" +f2 REGEXP "%wild%" +f4 = 'unclosed +(hasAny(f2, 1)) AND (f1 < 1) +f5 IN ("a_b%", "%wild%", f5, "a_b%", '') +f3 IN ("\t", -2.7) +f2 IN (f3, "xyz%", ' ', f3) +hasAll(f2, -10) +f5 = "\t") +f4 BETWEEN "xyz%" +f4 = NULL +f5 BETWEEN -1e-10 AND "\t" +f6 = -1e-10) +(f1 = -.5) OR (f5 = "a_b%") +f6 OR OR -2.7 +hasAll(f1, ' ') +f2 REGEXP 'O''Reilly' +f2 BETWEEN -1 AND -2.7 +f4 NOT ILIKE "\t" +AND f5 = ' ' +(f3 = "abc") AND ((f3 IN ('test', -10, "hello world", -3.5e-2, 10)) AND (f5 IN (-1e-10, ' ', 10))) +f1 IN (999999999999, "abc", 1e10, f6, 10) +f6 IN () +hasToken(f1, f4) +hasAny(f2, "%wild%") +f2 BETWEEN f5 AND f4 +f4 BETWEEN f1 AND ' ' +f4 NOT LIKE ' ' +f4 IN ("%wild%", "a_b%") +f3 >= -2.7 +f4 CONTAINS "xyz%" +(hasAny(f2, "hello world")) AND (f6 LIKE "hello world") +f3 === 999999999999 +(hasAll(f6, f6)) AND (f4 IN (-1e-10, 999999999999, -10, -.5)) +hasAny(f1, f2) +f6 IN () +f2 IN () +(f2 IN ("xyz%", 999999999999, "\t", "hello world")) OR (f2 IN (TRUE, 3.14)) +f6 BETWEEN 0 AND 10 +f2 BETWEEN f2 AND "\t" +f3 BETWEEN -3.5e-2 AND f3 +(f5 NOT REGEXP "\t") OR (f1 <> -1e-10) +f2 IN (0) +f1 <= 999999999999 +f3 BETWEEN f6 AND f1 +f4 NOT REGEXP 'O''Reilly' +has(f4, f4) +(f6 LIKE 'test') OR (f4 IN (0.0000000001, 1e10)) +f3 CONTAINS '' +f3 < TRUE +f1 IN ("a_b%", f5, 999999999999, "a_b%") +f1 IN [1 2 3] +f1 LIKE "\t" +AND f2 = f2 +hasAny(f2, -.5) +(f3 IN ('', -1e-10, f3, -.5)) AND (f4 CONTAINS "abc") +f2 IN ('O''Reilly', 999999999999, 10, 1e10) +f1 NOT LIKE 'test' +f2 = 'unclosed +f5 BETWEEN '' AND -1 +(f5 IN (-.5, "xyz%", 1, f3)) OR (((f2 IN (10, f3, 999999999999, 'test', f5)) OR (f6 LIKE "abc")) OR (hasAll(f2, 'O''Reilly'))) +f4 REGEXP '' +(((f5 BETWEEN TRUE AND f6) AND ((f3 = "a_b%") OR (f5 != "%wild%"))) OR (f5 BETWEEN f4 AND 1e10)) OR (f5 ILIKE 'test') +f2 = NULL +f6 = f1) +f3 == 'O''Reilly' +(f5 = "\n") OR (f5 BETWEEN "\n" AND f4) +NOT +hasAny(f5) +f5 IN [1 2 3] +f6 BETWEEN 999999999999 AND f5 +f5 = 'unclosed +f2 IN (0) +f5 BETWEEN -2.7 AND '' +f2 <> "\t" +((f1 IN (1e10, 3.14, -3.5e-2, f5, -1)) AND (f1 BETWEEN f1 AND f4)) AND (f1 != ' ') +f1 BETWEEN f3 AND f5 +f5 LIKE "abc" +(((f4 <= "%wild%") AND (f3 BETWEEN 'O''Reilly' AND "abc")) OR (f5 BETWEEN f4 AND .5)) OR (f3 < f5) +has(f1, f3) +f1 BETWEEN AND f2 +f6 BETWEEN 0 AND -1e-10 +((f2 BETWEEN -3.5e-2 AND .5) OR ((f1 > 999999999999) AND (f6 IN (0.0000000001)))) AND ((f2 IN ("\n", -3.5e-2, '', 6.02e23)) AND (f1 BETWEEN 0.0000000001 AND .5)) +f5 NOT REGEXP 'O''Reilly' +(f4 BETWEEN f5 AND 1) AND ((f4 NOT LIKE "xyz%") OR (f6 CONTAINS "hello world")) +f1 === -2.7 +f1 BETWEEN -3.5e-2 AND -3.5e-2 +has(f3, f4) +f2 IN ('test') +f1 IS NULL +f1 IN ("xyz%", f6, "a_b%", -3.5e-2, 'test') +f4 LIKE 'test' +f5 IS NULL +(f3 <> "a_b%") OR ((f4 != f2) AND (f6 NOT CONTAINS 'test')) +f2 IN () +f1 BETWEEN AND -1e-10 +f3 BETWEEN -1 AND "\t" +f6 >= "hello world" +f4 IN [1 2 3] +hasToken(f5, -1e-10) +f2 < 10 +(f3 < 3.14) AND (has(f6, 10)) +f1 IN () +f2 IN ("\t", 1, -2.7) +has(f2, f2) +f1 IN (f3, -1, -.5, 6.02e23) +hasAll(f1, ' ') +AND f3 = f6 +f6 IN (1e10, "hello world", f4, "abc", '') +f2 NOT CONTAINS "hello world" +f2 BETWEEN "%wild%" AND f4 +f6 = 'unclosed +hasToken(f3, 'test') +f2 IN ("abc", 0.0000000001, "a_b%", "\t") +f3 = NULL +f2 BETWEEN 'O''Reilly' AND 0 +f6 BETWEEN AND -3.5e-2 +(f3 <= -1e-10) OR ((f4 BETWEEN 999999999999 AND "\t") AND (hasToken(f5, 1e10))) +f2 LIKE +(f3 BETWEEN 'test' AND 6.02e23) AND (f1 >= "%wild%") +hasAll(f3, "%wild%") +f2 IN ('O''Reilly') +(f2 NOT LIKE "a_b%") AND (f5 BETWEEN .5 AND .5) +f4 IS NULL +f6 NOT ILIKE "a_b%" +hasToken(f5, '') +f5 NOT +(hasAll(f2, f4)) AND ((f3 >= -3.5e-2) OR (f6 IN (-10, 1))) +f2 < "\t" +f2 = "hello world") +f6 CONTAINS "xyz%" +f5 CONTAINS 'test' +f5 IN (6.02e23) +f1 IS NULL +has(f1, -2.7) +f3 BETWEEN AND 0 +f2 CONTAINS '' +AND f6 = .5 +f5 CONTAINS "%wild%" +(hasAny(f3, -1)) AND ((f1 IN ("hello world", -.5, "xyz%")) OR (f6 >= 0.0000000001)) +f2 IS NULL +f6 BETWEEN f6 +f5 NOT CONTAINS "\t" +f6 BETWEEN AND 999999999999 +f5 BETWEEN "hello world" AND -2.7 +f2 IN (10, "abc", "%wild%", f1, 1) +f5 IN (,) +f1 NOT CONTAINS "a_b%" +(f2 = -3.5e-2 +(f5 = -.5) OR (f5 IN (10, f6, ' ')) +hasAll(f5) +f6 NOT REGEXP "abc" +AND f6 = '' +f2 BETWEEN "%wild%" AND 'test' +has(f6, 3.14) +f2 IN (f4, 1e10) +hasToken(f6, "a_b%") +f2 NOT +f1 < 3.14 +(f3 BETWEEN 6.02e23 AND TRUE) OR (f3 NOT ILIKE '') +((f4 NOT ILIKE "a_b%") AND (hasToken(f3, -10))) OR (f6 BETWEEN f1 AND -3.5e-2) +f5 > 6.02e23 +f2 BETWEEN "xyz%" AND TRUE +hasToken(f5, f5) +f5 BETWEEN 1e10 AND "\n" +f5 IN () +f4 IN (,) +(f4 BETWEEN f1 AND 10) AND (((f1 BETWEEN 0.0000000001 AND f4) AND ((f6 == -1) AND (f4 >= TRUE))) OR (f4 NOT CONTAINS 'test')) +f3 = 'unclosed +hasAll(f1, "xyz%") +(f1 = -.5) OR (hasAll(f2, '')) +f5 != -1e-10 +f6 BETWEEN 3.14 AND 999999999999 +(f6 IN ("a_b%", "%wild%", 1, f3, 1)) AND (f3 LIKE "%wild%") +f1 CONTAINS ' ' +f3 == 'O''Reilly' +hasToken(f4, -3.5e-2) +f3 NOT ILIKE 'O''Reilly' +f6 NOT CONTAINS 'test' +has(f3, f1) +f2 NOT REGEXP "hello world" +f2 BETWEEN '' AND f6 +f4 NOT LIKE "hello world" +f5 BETWEEN "xyz%" AND 3.14 +(f5 IN (3.14, "\n", "%wild%")) AND (((f3 IN ('test', 'O''Reilly', f1, "xyz%")) OR (f2 BETWEEN 0 AND 1)) OR (f3 != 'O''Reilly')) +NOT +f5 NOT REGEXP "abc" +(f4 LIKE "\n") AND (f4 ILIKE "\t") +f1 BETWEEN AND -.5 +has(f4) +f6 >= -.5 +f6 OR OR "a_b%" +f6 BETWEEN f6 AND -1e-10 +(f5 = 10 +hasAll(f1, ' ') +f6 > '' +hasToken(f6) +f6 LIKE "a_b%" +f3 IN (TRUE) +f2 <> "%wild%" +f2 BETWEEN -.5 AND "xyz%" +AND f1 = 6.02e23 +f5 NOT CONTAINS 'O''Reilly' +(hasAll(f4, f4)) OR ((f5 <= 0.0000000001) AND (f2 IN (10, "xyz%", 999999999999, "\n"))) +f2 BETWEEN ' ' AND 3.14 +f2 BETWEEN 0 AND .5 +f4 IN (0, .5, f5, "\n", 999999999999) +hasAny(f5) +f1 = ' ') +(f4 IN ('', "\t", 'test', -3.5e-2)) OR (f1 < 0) +f6 IN (,) +f2 = "%wild%") +hasAny(f5, 0) +f5 IN () +f6 != f6 +(f4 = "\t" +hasToken(f3, 1e10) +(f4 BETWEEN "a_b%" AND f6) OR (f1 IN (-1e-10, f5)) +((has(f3, 0.0000000001)) OR (f2 IN (-1e-10, -2.7, -1e-10))) AND (((f6 BETWEEN f2 AND "%wild%") OR (f1 NOT REGEXP "abc")) AND (f3 <= f6)) +f4 == -.5 +f3 ILIKE 'O''Reilly' +NOT +f1 NOT +f4 BETWEEN 3.14 AND TRUE +f4 NOT +has(f6, -.5) +hasAll(f3, -1e-10) +((f4 CONTAINS "%wild%") AND ((hasToken(f5, "%wild%")) OR (f6 = 3.14))) OR (hasToken(f3, f2)) +f2 BETWEEN '' AND -1e-10 +f1 < 3.14 +f1 CONTAINS "%wild%" +f6 IN (TRUE, -1) +f5 LIKE ' ' +(f2 <> -2.7) AND (f1 BETWEEN f1 AND "\t") +f4 NOT +f3 CONTAINS ' ' +(has(f6, ' ')) OR ((f3 NOT ILIKE "\n") OR (f4 LIKE "a_b%")) +f2 NOT ILIKE "xyz%" +(f5 > "hello world") OR (hasToken(f6, "abc")) +f3 <= .5 +f3 BETWEEN 999999999999 AND -2.7 +hasToken(f2, TRUE) +f4 IN (10, -1, "a_b%", 10, 'O''Reilly') +f3 <= .5 +f5 = 'unclosed +f3 IN (f2, 0.0000000001, 'O''Reilly', f5) +f5 NOT ILIKE "\n" +f5 < -2.7 +hasAll(f5, "\n") +hasAny(f2, -2.7) +f3 < -.5 +f5 OR OR 3.14 +(hasAny(f3, 'test')) AND (f3 BETWEEN f2 AND -1) +f2 BETWEEN "abc" AND f2 +f4 CONTAINS "abc" +() +f3 REGEXP "\t" +f1 BETWEEN -2.7 AND f6 +f2 BETWEEN 999999999999 AND f2 +f4 BETWEEN "\n" AND "abc" +f4 == ' ' +NOT +f4 IN ("\t", f2, f5, -10) +(hasAny(f5, -1)) AND (f2 >= "a_b%") +f3 = +f5 NOT CONTAINS "xyz%" +(f5 IN (1e10, "\n", "a_b%", "%wild%")) OR (f2 <= 'test') +f4 REGEXP "a_b%" +(f2 IN (' ', 1e10, ' ', f2)) OR (f6 IN (6.02e23, "a_b%", -.5, TRUE, 10)) +has(f3, -2.7) +f3 = f3) +f4 LIKE "\t" +(hasAny(f1, f6)) AND (f5 <> "xyz%") +(f4 BETWEEN "a_b%" AND f6) OR (f2 < ' ') +(f5 BETWEEN 6.02e23 AND "abc") OR (hasAny(f3, "hello world")) +f4 = 'unclosed +f6 <> -3.5e-2 +(f6 = -3.5e-2 +f6 REGEXP ' ' +f2 BETWEEN f4 AND 1 +hasAll(f3, f4) +f6 ILIKE "%wild%" +has(f2, 6.02e23) +has(f3, 1e10) +f6 IN [1 2 3] +(hasAll(f3, "%wild%")) OR (f1 IN (-3.5e-2, "a_b%", "abc", 3.14, "xyz%")) +AND f1 = ' ' +f3 BETWEEN 'O''Reilly' AND 0 +f5 CONTAINS "a_b%" +f5 CONTAINS "\t" +f2 BETWEEN '' AND 10 +f5 BETWEEN TRUE AND "a_b%" +(f4 IN (10, 1e10, 999999999999, 1)) AND (f5 IN (1e10, 'test', "\t", f3)) +() +f1 >= -3.5e-2 +f5 BETWEEN 0 AND "abc" +f6 = 3.14) +f2 = "\n") +f4 ILIKE "hello world" +f3 IN (-10, 'test', 0) +f4 IN ("\n", 6.02e23, -3.5e-2, ' ', -10) +f1 BETWEEN AND 0 +f2 <= 0 +has(f1, f2) +(f5 BETWEEN -2.7 AND "\n") AND (f3 IN (1)) +(hasAll(f5, "a_b%")) OR (f2 BETWEEN -10 AND f1) +f6 BETWEEN "hello world" AND 6.02e23 +f5 = +f5 BETWEEN '' AND 1 +(f3 = f1) OR (f1 REGEXP '') +f6 BETWEEN -1e-10 AND .5 +f3 >= "a_b%" +(f5 BETWEEN -3.5e-2 AND 'test') AND (f1 BETWEEN 1e10 AND f6) +f5 IS NULL +f5 BETWEEN f1 +has(f4) +hasAny(f6) +hasAll(f3, 10) +() +() +f2 BETWEEN -1 AND f6 +f3 = "hello world" +f5 IS NULL +f1 = f4 +f4 IN (0.0000000001, -2.7, -10, f4, .5) +f6 IN (1) +f3 IN (f6, "hello world", "\t", "%wild%") +f2 NOT CONTAINS 'O''Reilly' +(f5 = 0.0000000001 +f4 IN (-2.7, 6.02e23, 0) +hasAny(f1, f1) +f5 BETWEEN 6.02e23 AND ' ' +f2 = +hasAll(f6, f4) +f3 = 'unclosed +hasToken(f1, "a_b%") +(f2 > "abc") OR (f3 ILIKE "\n") +(f5 = '' +(f2 IN (f3, "abc")) OR ((f1 < f5) OR (f3 REGEXP ' ')) +(f1 REGEXP "\n") AND (hasToken(f4, -10)) +f4 IN (f4, f2, 0.0000000001) +f1 <> "xyz%" +f2 CONTAINS '' +f6 NOT +(f1 = f1 +f2 NOT ILIKE "%wild%" +f1 = "xyz%" +f4 IN (TRUE, f2, .5, 6.02e23, "\n") +f6 ILIKE '' +f5 IN [1 2 3] +f5 = 'unclosed +f1 BETWEEN "hello world" AND 999999999999 +f2 NOT LIKE '' +hasToken(f4, 1) +f4 < "xyz%" +f3 < 0 +(f1 IN ('', TRUE, -1, 6.02e23)) AND (f1 <= 'O''Reilly') +f1 IN (,) +f6 BETWEEN "hello world" AND "xyz%" +f6 BETWEEN f4 +f4 = 'unclosed +f6 = 'unclosed +f6 == f4 +f6 BETWEEN 1 AND "xyz%" +f3 BETWEEN "\n" AND 10 +f2 == TRUE +f4 NOT REGEXP "hello world" +f4 == 6.02e23 +f5 IN (,) +(f5 BETWEEN 1 AND 'test') AND (has(f5, f5)) +f4 IN (,) +f4 <> -10 +f4 IN (f4, -.5, "%wild%") +f4 IN (f6, -3.5e-2, -1, 999999999999) +f4 == "a_b%" +(f6 != "\t") AND (f3 BETWEEN 1 AND f4) +f2 BETWEEN 10 AND f1 +f4 NOT REGEXP 'O''Reilly' +f2 IN ("hello world", -3.5e-2) +((hasAny(f3, "%wild%")) AND ((hasToken(f6, -10)) OR (has(f6, '')))) AND (f5 BETWEEN -1 AND "%wild%") +f1 BETWEEN 0.0000000001 AND "\n" +f4 BETWEEN '' +f1 NOT REGEXP "\t" +f1 NOT REGEXP "hello world" +f1 IN ("\t", f5, "hello world", 'test', 3.14) +f3 BETWEEN f1 AND 1e10 +(f3 = 1 +f6 IN (-10, '', 3.14) +hasAll(f4, -10) +f2 NOT +f3 NOT +f4 < "\n" +f6 BETWEEN f1 AND "%wild%" +f2 BETWEEN "xyz%" AND f2 +(f1 == "hello world") AND (f5 BETWEEN f2 AND -10) +has(f3, "xyz%") +f5 IN (-.5) +() +f5 = 'unclosed +f3 = .5 +f3 <= 0.0000000001 +((f5 LIKE "\t") AND (f2 == -2.7)) OR ((hasToken(f3, 1)) AND ((f3 BETWEEN f1 AND f2) OR (f2 IN (.5, 6.02e23, ' ', -1e-10, "\t")))) +f1 = +f2 OR OR f1 +f4 <= ' ' +has(f6, 999999999999) +hasAll(f5, 10) +hasToken(f5, 'test') +f3 NOT REGEXP "abc" +f6 BETWEEN "a_b%" AND -1e-10 +f2 NOT +f4 BETWEEN -2.7 AND 999999999999 +hasAll(f3, f5) +has(f4, f6) +(hasAny(f2, 'O''Reilly')) AND (hasAll(f2, "hello world")) +f2 IN (f6, ' ', f6, .5) +f4 <= '' +f2 BETWEEN '' AND TRUE +f4 = NULL +f1 IN (999999999999, ' ', "%wild%", f4, -1) +f5 BETWEEN 1 AND 3.14 +(f4 = 1 +hasAll(f3, f3) +f6 IN ("hello world", -2.7) +NOT +(f4 LIKE "%wild%") OR (((f4 ILIKE "abc") AND (f6 = 'test')) AND (hasAll(f2, "abc"))) +hasToken(f2, 1) +AND f6 = f4 +hasAny(f2, -.5) +f4 IN (f2, 1e10, f3, f3) +f6 NOT REGEXP "abc" +f6 > 1e10 +f5 IN (f1, -1, 0.0000000001) +f5 BETWEEN 0.0000000001 AND "\t" +hasAll(f4, 1e10) +f1 = NULL +f5 NOT ILIKE "\t" +hasAny(f3, -.5) +f2 > 'O''Reilly' +f6 NOT CONTAINS "\n" +f4 IN [1 2 3] +((has(f2, .5)) OR ((f2 BETWEEN "%wild%" AND 10) AND (hasToken(f3, 999999999999)))) OR (f5 CONTAINS "hello world") +f5 BETWEEN "\n" AND ' ' +hasToken(f5, f3) +(hasAny(f6, -.5)) AND (f5 == 1) +f1 BETWEEN TRUE +f3 NOT REGEXP "xyz%" +hasToken(f1, f2) +(f6 = 0) OR ((hasToken(f6, 1e10)) OR (f6 BETWEEN 1e10 AND f4)) +f6 OR OR 'O''Reilly' +f5 >= 6.02e23 +f2 <> "xyz%" +f3 BETWEEN 0.0000000001 AND f1 +((f4 NOT ILIKE 'O''Reilly') AND (f3 NOT CONTAINS "a_b%")) AND (f6 BETWEEN -10 AND 0) +(f5 NOT LIKE 'O''Reilly') AND (f4 NOT LIKE "\t") +(f6 = f4 +(hasAll(f4, f4)) AND ((f1 ILIKE 'O''Reilly') AND (f4 IN (f6, "abc", 10, 999999999999))) +f4 BETWEEN '' AND 6.02e23 +f3 BETWEEN AND f2 +f5 BETWEEN 0.0000000001 AND 'O''Reilly' +(f4 = "xyz%" +f1 BETWEEN "\t" AND 1e10 +f4 BETWEEN 0.0000000001 +f2 LIKE "abc" +f4 BETWEEN 6.02e23 AND .5 +f1 BETWEEN AND ' ' +f5 OR OR f4 +f4 OR OR f4 +has(f5, f4) +f6 BETWEEN "\n" AND -2.7 +(f4 BETWEEN 0 AND 10) AND (hasAny(f2, -2.7)) +f4 BETWEEN f6 AND -1e-10 +f4 IN (-3.5e-2) +f5 REGEXP "xyz%" +f1 ILIKE "\n" +f5 = f4) +f3 IN (-3.5e-2, 6.02e23, 0.0000000001, '', '') +() +f6 OR OR f2 +f3 BETWEEN TRUE AND 'O''Reilly' +f4 BETWEEN 'O''Reilly' AND f5 +f6 IN ("a_b%", -1, f3) +has(f2, 0) +f1 = +f5 IN (0) +f1 = TRUE +f3 REGEXP ' ' +hasToken(f2, f1) +f6 REGEXP "%wild%" +f2 = 'unclosed +f5 = +f3 NOT ILIKE '' +f4 = +(f4 IN (TRUE, 1)) OR ((f2 NOT LIKE 'O''Reilly') OR (hasToken(f4, "abc"))) +has(f4) +f2 IN (f2, ' ', f2, 'O''Reilly', f6) +(f6 IN (f2, .5, "abc")) AND ((hasAll(f3, "xyz%")) OR (f4 <> "a_b%")) +f3 NOT CONTAINS ' ' +f4 BETWEEN -3.5e-2 AND 3.14 +hasToken(f3, f2) +f5 == f4 +f5 NOT ILIKE "hello world" +has(f1, 10) +f3 < ' ' +f2 CONTAINS "a_b%" +f1 IN (-10, 0.0000000001, "hello world") +f3 BETWEEN TRUE AND -.5 +f6 BETWEEN 0.0000000001 +f2 IN (f4, "a_b%", 1) +f5 BETWEEN 1e10 AND .5 +f5 >= f5 +f3 BETWEEN AND f4 +f4 NOT ILIKE "\n" +hasAny(f6, f1) +f4 NOT +f6 IS NULL +((f3 ILIKE ' ') AND (f3 IN ("abc", 1, "%wild%", 'O''Reilly'))) AND ((f4 NOT LIKE "xyz%") OR (f3 NOT ILIKE "\n")) +(f3 BETWEEN "a_b%" AND 3.14) OR ((hasAll(f6, 0)) OR (f6 IN (-1e-10, 0, ''))) +f3 NOT CONTAINS "hello world" +f5 BETWEEN '' AND -1 +f6 === 3.14 +f5 BETWEEN -2.7 AND "a_b%" +f5 BETWEEN f2 AND 1 +f2 IN (,) +f1 NOT CONTAINS 'test' +f6 NOT ILIKE "\n" +hasAny(f3, -1) +f6 IN (999999999999, f1, .5, f1, f2) +hasToken(f4, "%wild%") +f4 >= f4 +f5 IN (f2, "abc", "a_b%") +f3 >= f4 +f2 = +f3 IN (f2, 6.02e23, 999999999999, "%wild%", f5) +f4 IN (-1, 1) +f1 < 3.14 +f6 REGEXP "\n" +f4 LIKE '' +f6 NOT LIKE "xyz%" +f6 IN (f1, 3.14, 0.0000000001, 'test') +f4 IN (-1, -1, -1) +f3 IN [1 2 3] +f6 <= 'O''Reilly' +f5 IN ("%wild%", f1) +((f6 <> "\t") AND (hasToken(f2, 'test'))) OR (f2 BETWEEN 'O''Reilly' AND '') +has(f6, f5) +(f3 BETWEEN -10 AND 'test') AND (f3 IN (TRUE, "\t", "abc", -1, -1)) +f3 IN (1, "hello world") +(hasToken(f6, f5)) AND (f1 IN (0, "\n", .5)) +f5 >= 999999999999 +f5 == "\t" +() +f1 <= "xyz%" +f4 ILIKE "a_b%" +f2 > 0.0000000001 +f4 BETWEEN "\t" AND f6 +f2 BETWEEN ' ' AND f5 +f6 LIKE +f6 BETWEEN 0.0000000001 AND 3.14 +f4 NOT +f3 LIKE 'O''Reilly' +f3 != f1 +f6 IN (6.02e23, TRUE, 'test', '', "xyz%") +(f2 >= -1) OR (hasToken(f5, f1)) +hasAny(f2, f2) +f3 ILIKE 'O''Reilly' +f4 NOT +f4 BETWEEN .5 AND -1e-10 +f2 IN (' ', "%wild%", "\n") +f2 BETWEEN f1 AND 6.02e23 +AND f2 = .5 +f6 BETWEEN AND '' +f6 IN (1e10, 0, f2, -3.5e-2, f4) +f2 NOT ILIKE 'O''Reilly' +((f4 BETWEEN -2.7 AND "abc") AND (f4 ILIKE ' ')) AND (f2 BETWEEN TRUE AND -.5) +(f3 CONTAINS "\n") OR (has(f5, 'test')) +f5 = 'unclosed +f6 IN (-3.5e-2, "\t", TRUE, f6) +hasAll(f1, 0.0000000001) +f4 NOT ILIKE "hello world" +f4 == 999999999999 +f4 = 'unclosed +((f5 = -2.7) OR (f3 LIKE "hello world")) AND (f2 LIKE "xyz%") +f2 IN [1 2 3] +f3 >= "abc" +f3 == 1 +f1 CONTAINS "hello world" +f3 LIKE 'O''Reilly' +f4 BETWEEN 1e10 AND 6.02e23 +f5 ILIKE "hello world" +AND f3 = 6.02e23 +has(f3, -10) +f5 NOT CONTAINS "\t" +f6 BETWEEN -10 AND -1 +hasAll(f4, 1e10) +() +f6 BETWEEN AND -3.5e-2 +f1 REGEXP 'test' +f5 BETWEEN 0 AND 6.02e23 +f3 <= 1e10 +f4 = 1e10 +f3 === f6 +hasAll(f1, 1e10) +f2 BETWEEN -.5 AND f3 +f3 BETWEEN "xyz%" AND -1 +hasAny(f6, 'test') +f3 IN (3.14, f6) +f3 IN ("hello world", f3, 10, 10, f4) +f5 OR OR TRUE +(f6 IN ("hello world")) OR (hasToken(f2, '')) +((f5 = -1e-10) AND (f2 NOT LIKE "\t")) AND (f3 BETWEEN f6 AND 0) +f2 BETWEEN .5 AND '' +hasAll(f4, 1) +f3 <= 1 +hasAny(f6, 999999999999) +f4 != 1e10 +hasToken(f4) +f1 REGEXP "abc" +f2 IN [1 2 3] +f6 REGEXP "abc" +f5 > 1 +has(f6, 0) +f3 OR OR TRUE +f2 NOT REGEXP "hello world" +f6 BETWEEN 1 AND 6.02e23 +hasToken(f4, -2.7) +f3 CONTAINS "abc" +hasAny(f2, '') +f4 = NULL +f2 BETWEEN f5 AND f5 +(f4 BETWEEN .5 AND "a_b%") OR (has(f5, "\t")) +f5 NOT +f1 IN ('test', f6) +f3 IN (f2, 999999999999, -10, 999999999999) +((f5 = TRUE) OR (hasAll(f3, "a_b%"))) AND (hasToken(f3, 0)) +f1 == f5 +f4 = "\n" +(f1 != f2) AND (has(f2, 3.14)) +f2 > f4 +f4 IN (0, 1, -10) +((f2 IN (10, f2, 6.02e23, 'test')) AND (f4 < 999999999999)) OR ((f6 IN (.5)) OR (f3 REGEXP "\t")) +f2 REGEXP '' +f1 BETWEEN f1 AND 'test' +f1 <= f6 +f2 > -2.7 +f5 IN ("abc", "hello world") +f4 BETWEEN f5 +f1 BETWEEN f1 AND "hello world" +f1 IN (-1, "a_b%", 'test', -2.7) +f4 BETWEEN -2.7 AND 0.0000000001 +f6 IN ("abc", -2.7, 0, f5) +((hasAll(f4, f2)) OR (f1 <> 0)) OR (f6 > "hello world") +f3 NOT CONTAINS 'test' +(f6 IN (f4, "abc", f1, f4, -1)) AND ((hasAny(f3, '')) AND (f6 >= "a_b%")) +f5 <> f4 +(hasToken(f3, f2)) OR (((f2 IN (1, "a_b%", ' ', -1, ' ')) OR (has(f2, "hello world"))) OR ((f4 LIKE "%wild%") AND ((f5 != 'test') AND (f4 == 3.14)))) +f1 IN (1e10, f5, f6, 1) +has(f6, "\t") +f1 BETWEEN .5 AND 6.02e23 +f1 === -.5 +f4 ILIKE "a_b%" +(f5 BETWEEN ' ' AND f1) AND (f2 NOT CONTAINS "xyz%") +f3 = 'unclosed +f2 NOT CONTAINS 'O''Reilly' +f6 < 6.02e23 +f2 IN (f2) +f3 NOT LIKE "a_b%" +f1 REGEXP "hello world" +(hasAll(f2, 0.0000000001)) AND (hasAll(f3, f5)) +(f4 BETWEEN 'test' AND 1e10) OR (f4 BETWEEN ' ' AND f3) +has(f6, f1) +hasAny(f1) +f1 = f4 +f6 IN (1e10) +hasAny(f4) +hasToken(f1) +hasToken(f3, 999999999999) +f1 BETWEEN 0.0000000001 AND .5 +(f2 BETWEEN "\n" AND 10) AND (hasAny(f5, f2)) +f5 <> -10 +has(f5) +f2 <> f2 +f5 IN ("\t", -3.5e-2, 'O''Reilly') +f3 OR OR 0 +hasAny(f5, 999999999999) +AND f5 = .5 +hasToken(f3, f4) +hasAll(f2, f6) +f2 IN (-10, '', f1, 'test') +f2 BETWEEN -1 AND f2 +f4 LIKE +f6 <= 3.14 +f5 = 'unclosed +f2 >= f5 +f1 NOT ILIKE ' ' +(f4 == -.5) AND (f3 > -.5) +f1 BETWEEN "\t" AND 1e10 +f4 OR OR f4 +f3 IS NULL +f2 = f2 +() +hasAll(f4, "hello world") +f6 IN (3.14, f2, "xyz%", 'test') +f1 = NULL +f3 BETWEEN AND 0 +(f1 = ' ' +f5 == "xyz%" +f3 IN (,) +f6 IN ("xyz%", ' ', "xyz%", f3) +f3 BETWEEN f2 AND ' ' +f1 REGEXP '' +f3 BETWEEN "\t" AND f1 +f4 LIKE "xyz%" +f5 LIKE +f4 IN (,) +f1 IN (-10, f5, "xyz%", -10) +(f4 CONTAINS 'test') AND (((f6 BETWEEN "abc" AND .5) OR (f4 LIKE "hello world")) OR (f4 IN (TRUE))) +f3 IN (TRUE, 'test', '', f6) +f3 BETWEEN 'O''Reilly' AND 3.14 +f5 NOT +NOT +hasAll(f5) +(f5 = "abc" +(f3 NOT CONTAINS 'test') OR (hasAll(f1, "abc")) +f3 BETWEEN .5 AND -2.7 +f4 = f3) +f1 NOT LIKE "\n" +f3 BETWEEN -3.5e-2 AND "\n" +f2 = ' ') +f4 BETWEEN AND "a_b%" +f4 IN (f4, .5, "hello world", -1) +f4 LIKE "hello world" +() +f2 IN ("\n", 999999999999, f5) +f6 IN ("hello world", ' ', "%wild%", "%wild%", f4) +(f2 BETWEEN "%wild%" AND "hello world") AND (f6 > 1) +f3 = NULL +f6 IN (f5, 10, "hello world", -3.5e-2, f1) +f1 NOT +(f2 IN (-1, 10, f4)) OR (f4 > 0.0000000001) +f6 REGEXP "hello world" +f4 < -10 +f2 CONTAINS "\n" +(f3 BETWEEN 1e10 AND ' ') AND (f4 == ' ') +f5 LIKE ' ' +f2 BETWEEN 10 AND f4 +f5 == -1e-10 +((f2 <> f4) OR (f2 BETWEEN f1 AND 1e10)) OR (has(f3, 0)) +f6 NOT REGEXP "\n" +f6 != 'O''Reilly' +f1 IN (-10, -2.7) +has(f4, "xyz%") +f5 IN (-2.7, 3.14, 6.02e23) +f2 BETWEEN 10 AND '' +f4 CONTAINS "\t" +f2 BETWEEN 'test' AND -1e-10 +f6 BETWEEN f4 AND ' ' +hasAll(f6) +hasAll(f6, 10) +f6 != f2 +f6 BETWEEN 3.14 AND f3 +f4 != ' ' +f3 IN (-3.5e-2, f1) +hasAll(f2, 'O''Reilly') +(f6 BETWEEN f3 AND 6.02e23) OR (hasToken(f6, f5)) +f6 BETWEEN f6 AND "hello world" +f1 = NULL +(f4 = -3.5e-2 +(f6 > f5) AND (f6 = 6.02e23) +f3 == 0 +f5 == -.5 +f2 IN (f3, 10) +f2 BETWEEN 'test' +hasAll(f2, 999999999999) +f6 IS NULL +f1 IN () +f4 IS NULL +f2 BETWEEN f4 AND -1 +f1 < -3.5e-2 +f5 != -3.5e-2 +f6 NOT +f5 BETWEEN "a_b%" +f3 >= 3.14 +has(f6, -1) +f3 BETWEEN AND 10 +f2 BETWEEN AND f4 +f3 NOT +f4 IN (10, "\n", "xyz%") +(f1 NOT CONTAINS "a_b%") OR (f6 != f5) +f1 IN [1 2 3] +f3 IN (-1, -3.5e-2, "\t", f5) +f3 LIKE 'O''Reilly' +f1 IN ("hello world", -.5, "xyz%") +f6 LIKE +((f4 BETWEEN f6 AND "xyz%") OR (f3 != "xyz%")) AND (f6 NOT LIKE 'O''Reilly') +f4 <= 3.14 +f1 <= -10 +f2 LIKE +f3 IN (f3) +f3 LIKE +has(f1, f2) +f5 LIKE +f4 NOT CONTAINS "abc" +f2 NOT LIKE "\n" +f5 BETWEEN 0.0000000001 AND 'O''Reilly' +f3 <> 0 +() +f4 REGEXP "abc" +f6 IN [1 2 3] +has(f5, 3.14) +f3 BETWEEN "\t" AND -.5 +f4 BETWEEN -2.7 AND 0 +f5 BETWEEN AND -1 +f6 NOT ILIKE ' ' +f3 IN (' ', -2.7, "a_b%", .5, "\t") +f1 CONTAINS "%wild%" +hasAll(f4, "\t") +f6 = NULL +f1 BETWEEN 6.02e23 AND 1e10 +f3 IN (1) +f1 IN (f2, f3, 999999999999, f5, -2.7) +f5 BETWEEN 10 +hasAny(f3, 0.0000000001) +f1 BETWEEN AND "hello world" +hasAny(f6, f4) +f3 < f3 +NOT +f4 BETWEEN f1 AND "xyz%" +f1 BETWEEN 10 AND "a_b%" +f1 = +f4 BETWEEN 'O''Reilly' AND 1 +hasAll(f2, 0) +f3 IN (1e10, 0) +f5 BETWEEN '' AND -10 +f1 IN (-1e-10) +f4 IN (3.14) +NOT +f5 = ' ' +f1 IN (1, 0) +(f6 IN (-10, "%wild%", f1)) AND (f2 BETWEEN f4 AND TRUE) +f4 BETWEEN "xyz%" AND f2 +f5 NOT ILIKE "abc" +f6 BETWEEN '' AND 1e10 +f5 IS NULL +f1 IN (f4, 999999999999) +f5 IN (-.5) +hasAny(f1, f1) +f2 IN ('O''Reilly', 999999999999, f1, ' ') +f1 BETWEEN 'O''Reilly' AND "a_b%" +hasAll(f3, TRUE) +f3 = "a_b%" +f3 BETWEEN AND "\n" +f3 != 999999999999 +f6 BETWEEN "abc" AND -2.7 +f1 ILIKE "a_b%" +((f1 REGEXP "abc") AND (f2 IN (f1, "\n", f1, f6, 'O''Reilly'))) OR (f4 != "\n") +f4 BETWEEN "a_b%" AND f4 +hasToken(f4, -1e-10) +f3 <> f5 +f5 != -1e-10 +f5 >= 1 +hasAny(f5, 'test') +f6 >= '' +(f1 BETWEEN 0 AND f6) OR (f4 BETWEEN f3 AND 1e10) +(f5 CONTAINS "abc") AND (f4 NOT LIKE "hello world") +f3 != '' +(f3 LIKE "%wild%") AND ((hasAny(f2, 'test')) AND ((hasToken(f4, 0)) AND (f1 = 999999999999))) +(hasAll(f5, "xyz%")) OR (has(f4, "hello world")) +(f5 = f3 +f2 == f6 +f6 CONTAINS "%wild%" +f6 IN (f4, 'O''Reilly', 0.0000000001, 1, f5) +f6 <> f2 +((f1 ILIKE "%wild%") OR (f3 == "%wild%")) OR (f5 NOT LIKE 'test') +f2 NOT LIKE 'O''Reilly' +hasAny(f1) +f4 != f2 +f3 >= TRUE +f5 < 0 +f4 IN (,) +f2 NOT LIKE "\n" +f5 NOT REGEXP "%wild%" +f3 BETWEEN "%wild%" AND 'O''Reilly' +f5 IN ("\t", f3, 1e10, 1e10) +f4 != -.5 +f4 LIKE +f4 BETWEEN 0.0000000001 AND TRUE +hasAny(f2, -.5) +f5 === 6.02e23 +f1 LIKE 'test' +f4 IN (10, 'O''Reilly', -1e-10) +f6 < -3.5e-2 +f2 NOT REGEXP "abc" +f1 == 6.02e23 +f4 = +f4 <= ' ' +f5 = NULL +AND f3 = -.5 +(hasToken(f5, -1e-10)) AND (f2 ILIKE "xyz%") +f3 CONTAINS "\n" +f1 NOT +f3 BETWEEN -1 AND 1 +f2 BETWEEN f3 AND TRUE +f3 BETWEEN AND "abc" +f2 === f5 +(f2 IN (f5, "%wild%", ' ', f5, "a_b%")) AND (f3 == ' ') +has(f2) +f5 BETWEEN f5 AND f3 +f6 IN (0.0000000001, 0.0000000001, 6.02e23) +f3 IN (f1, f5, 'test', 10) +f4 ILIKE 'O''Reilly' +f6 REGEXP "\n" +(f6 BETWEEN 'test' AND -2.7) AND (f4 NOT LIKE "abc") +hasAll(f4, 'test') +AND f5 = -3.5e-2 +f3 NOT REGEXP "\n" +f2 < -10 +(f5 NOT LIKE "a_b%") OR (f3 BETWEEN ' ' AND "\n") +f6 != 0.0000000001 +f3 IN (f3) +hasToken(f1, 999999999999) +(hasAll(f3, "abc")) OR (f5 REGEXP "a_b%") +(f6 BETWEEN .5 AND 6.02e23) AND (f6 == -10) +f6 IN (-2.7, f6) +f3 IN (f6, 999999999999, f3, f3) +f3 = f2 +f6 IN ("\t") +f4 = -.5) +f1 BETWEEN f5 AND 10 +f1 NOT CONTAINS '' +(f4 BETWEEN f5 AND f5) OR (f6 BETWEEN -1e-10 AND f1) +NOT +f4 LIKE +(f2 IN ('test', f2, f5, -2.7)) OR (((f2 BETWEEN -10 AND f2) AND (f6 <> 'O''Reilly')) OR (f4 IN (f1))) +f4 NOT REGEXP "\t" +f1 = -1 +hasAll(f4, ' ') +NOT +f1 = 'unclosed +hasAny(f4, f5) +f2 ILIKE "%wild%" +f6 = 'unclosed +f6 BETWEEN 1 AND f2 +has(f2, 'O''Reilly') +f3 NOT +has(f3, 6.02e23) +f4 BETWEEN AND -10 +f5 != ' ' +f1 <> 3.14 +f4 ILIKE "abc" +hasAll(f1, "hello world") +f3 IN () +f6 < "abc" +f1 = 1e10 +f3 IN ("\n", 'O''Reilly', f3, '') +(f6 IN (-1e-10, -.5, f4, f4)) OR (f1 > 0) +f5 IN (6.02e23) +f5 < f1 +has(f2) +NOT +f2 NOT ILIKE "abc" +(f6 = 10 +hasAny(f1, f1) +f4 IS NULL +(f5 < TRUE) OR (f5 <= 10) +f5 IN (-10, 6.02e23, 6.02e23, f6) +hasToken(f6, 'O''Reilly') +f5 ILIKE 'O''Reilly' +f4 === "\t" +f2 IN [1 2 3] +f1 BETWEEN f2 +(f3 = -3.5e-2) AND (f4 IN (.5, "abc", f2)) +f1 IN ("hello world", -3.5e-2, 999999999999, -2.7, '') +NOT +(f5 LIKE "abc") OR (f5 LIKE 'O''Reilly') +f3 IN ("xyz%", 10, ' ', -10, "%wild%") +f5 BETWEEN "xyz%" AND 6.02e23 +has(f1, -3.5e-2) +has(f5, 1) +f6 LIKE "\t" +hasAll(f1, 'O''Reilly') +f4 === -2.7 +f4 NOT +f4 NOT REGEXP ' ' +f1 <> 10 +f1 IN (TRUE, 3.14, f2, ' ', f1) +f6 BETWEEN 'O''Reilly' AND 'O''Reilly' +f4 REGEXP "\t" +f5 IN () +f2 NOT ILIKE 'O''Reilly' +f2 = -3.5e-2) +f5 BETWEEN '' AND '' +(f5 IN (3.14, "abc")) AND (f1 > "%wild%") +has(f3, '') +f1 = +f5 BETWEEN f5 AND "abc" +f2 IN (6.02e23, 0.0000000001, 6.02e23) +f6 IN (-2.7, 999999999999) +(hasAll(f4, f1)) AND (hasToken(f1, -1e-10)) +NOT +f1 IN (-3.5e-2, "\n", 1e10, '') +hasAny(f4, -1e-10) +(f6 = f3 +f3 > -.5 +f2 IN () +f2 BETWEEN f2 AND 1 +f2 = f6) +f1 NOT ILIKE ' ' +f5 === 1 +((f3 NOT CONTAINS "a_b%") AND ((f3 REGEXP "a_b%") AND (f6 BETWEEN "a_b%" AND "\n"))) AND (f2 BETWEEN 0 AND 999999999999) +f4 IN () +f1 CONTAINS "\n" +(hasAll(f5, "%wild%")) AND (f4 <> 6.02e23) +f4 != "\n" +(f5 NOT REGEXP ' ') AND (f2 <= -1e-10) +f4 IN ("%wild%", TRUE, 1e10, "a_b%", -1) +f5 REGEXP "xyz%" +f2 IN (0, f6, "\n", 1, -10) +f4 == 'test' +f2 BETWEEN "\t" AND 1 +f5 LIKE '' +hasAll(f6, "xyz%") +f3 = NULL +has(f2) +hasToken(f4, "%wild%") +f4 NOT +hasAny(f1, -2.7) +f3 NOT CONTAINS 'O''Reilly' +f1 BETWEEN -.5 AND 999999999999 +f4 == f2 +() +f1 BETWEEN TRUE AND 999999999999 +f1 IN (' ', -.5, f3, -3.5e-2) +f1 IS NULL +f3 BETWEEN -10 AND 0.0000000001 +(f2 BETWEEN 1 AND -10) AND ((f2 = 3.14) OR (f1 < "\n")) +f6 <> "\t" +f4 ILIKE ' ' +(f3 IN (0)) OR (((f2 IN (999999999999, '')) AND (f4 != TRUE)) AND (f1 IN ('O''Reilly'))) +f5 BETWEEN "abc" +f6 BETWEEN -.5 AND 1 +f2 LIKE "\n" +f2 = TRUE) +f5 BETWEEN 10 AND 0.0000000001 +f2 = +f5 < -2.7 +(f3 LIKE "xyz%") OR (f5 NOT ILIKE "\t") +f3 LIKE "abc" +f6 IN ('', f6, 'test') +((f1 == 1) AND (f2 BETWEEN 0 AND -2.7)) OR (f1 IN (0, "\t", 1, .5, f5)) +f1 = +f4 != 6.02e23 +hasAll(f6, -1e-10) +(f4 IN (f5, -3.5e-2, 0, 6.02e23, 0)) OR (has(f4, 1)) +f4 IN (,) +f6 OR OR 999999999999 +f1 IN (10, 10, 6.02e23, 1, f1) +f3 IN (-1e-10, "a_b%", f1, "xyz%") +f6 ILIKE "\n" +f6 NOT LIKE "abc" +f5 IN (,) +f2 BETWEEN TRUE AND 0 +f4 = f1 +f3 BETWEEN -3.5e-2 AND "%wild%" +f6 BETWEEN 3.14 AND -1e-10 +f4 <= 1e10 +f5 LIKE +f5 LIKE "a_b%" +f3 = +f5 IN (,) +(f4 REGEXP ' ') OR (f2 NOT REGEXP "abc") +f1 IN ('O''Reilly') +hasAny(f5, ' ') +f5 != f6 +f2 > "%wild%" +f1 === 6.02e23 +f6 BETWEEN AND TRUE +hasToken(f3) +has(f3, -2.7) +(f4 ILIKE "xyz%") AND ((f1 IN (-2.7, f3, -1, ' ')) OR (f3 IN ("hello world", 1, f1))) +f3 <> f6 +f4 NOT ILIKE '' +f1 BETWEEN "a_b%" AND '' +f5 NOT REGEXP "\n" +f5 === 1e10 +f2 BETWEEN -1 +f3 == 0.0000000001 +f4 <= 0.0000000001 +hasToken(f5, "\n") +f5 === -1 +f2 BETWEEN f1 AND "abc" +(f4 BETWEEN "abc" AND -1) AND (f1 BETWEEN "xyz%" AND -1e-10) +f3 BETWEEN f4 AND .5 +f6 === .5 +f3 BETWEEN f3 AND f4 +f2 IS NULL +f2 = NULL +f1 < 0.0000000001 +f1 < '' +(f3 CONTAINS "abc") AND ((f5 = -.5) OR (f4 IN (-3.5e-2, f1))) +f3 BETWEEN "\n" AND -1 +NOT +f2 ILIKE "\n" +has(f6, 'O''Reilly') +(f6 <> "hello world") AND ((f3 NOT CONTAINS "%wild%") OR (hasToken(f2, -1e-10))) +f5 NOT LIKE 'test' +f6 = 'unclosed +f3 BETWEEN f6 AND "abc" +f1 BETWEEN -1e-10 AND "\t" +f1 NOT ILIKE 'test' +f6 >= f3 +f5 BETWEEN f5 AND 'O''Reilly' +f4 != TRUE +f6 = 3.14) +f2 <> f2 +f5 BETWEEN "%wild%" AND 0.0000000001 +f1 IN [1 2 3] +f5 ILIKE 'test' +f2 BETWEEN f6 +(f1 BETWEEN "\n" AND f5) AND (f6 < "\n") +f3 == 6.02e23 +f3 < f2 +f1 BETWEEN -1e-10 AND TRUE +hasAll(f4, f6) +f2 BETWEEN f2 AND f1 +f2 ILIKE ' ' +f1 CONTAINS 'test' +(f6 IN ("abc")) OR (has(f5, f1)) +f3 NOT LIKE "%wild%" +f6 IN (TRUE, "\t") +hasToken(f2) +f4 NOT CONTAINS '' +f6 BETWEEN "hello world" AND "%wild%" +f1 NOT ILIKE ' ' +hasAny(f5, -1e-10) +f4 OR OR "a_b%" +(f4 = "%wild%" +hasToken(f6) +() +f5 = +f6 === 999999999999 +f5 IN (1e10, ' ', -1) +f3 NOT CONTAINS "a_b%" +f3 CONTAINS 'test' +has(f1, f5) +f3 <= 10 +f3 = +f1 CONTAINS "\t" +f6 IN (,) +f4 IN (f3, 3.14, f6, 3.14, 'test') +(hasAll(f4, f3)) OR (f3 IN (1, "\n", -1e-10, "hello world")) +f2 IN [1 2 3] +(has(f6, f6)) OR (f5 IN (-1, 'test', -2.7, 'test', 1)) +f3 BETWEEN f2 AND "xyz%" +f6 BETWEEN "\n" AND .5 +f2 = 'unclosed +f1 BETWEEN AND -3.5e-2 +f4 IN (f5, -.5, "abc", 10) +f4 BETWEEN ' ' AND f2 +(f2 = -2.7 +f4 NOT CONTAINS "\n" +hasAny(f6, -.5) +f3 BETWEEN "\n" AND f4 +f5 <= f4 +f5 NOT LIKE "hello world" +hasAll(f3, "xyz%") +f5 BETWEEN "hello world" AND 'O''Reilly' +(f4 BETWEEN f2 AND "\t") OR (f2 <= "\n") +hasToken(f5, f6) +has(f4, f4) +f5 BETWEEN 3.14 AND 'O''Reilly' +(f5 IN ('test')) AND ((f3 IN ("hello world", 1e10, f3, 10)) OR (f3 BETWEEN 'O''Reilly' AND "hello world")) +f4 BETWEEN TRUE AND -10 +f3 IN (-3.5e-2, f4, "hello world", 10) +f5 <> .5 +NOT +f5 OR OR f3 +f5 BETWEEN 999999999999 AND 1e10 +f4 IN () +f4 BETWEEN TRUE AND f2 +has(f4, "\t") +AND f2 = 999999999999 +hasToken(f5, 'O''Reilly') +f4 IN ('', "hello world", f5, f2) +f2 IN ('O''Reilly', "hello world", "abc", "xyz%") +f6 BETWEEN AND "%wild%" +f6 = +f5 OR OR "a_b%" +f5 CONTAINS "\n" +f5 != 999999999999 +f2 IN (0, "hello world", .5) +f6 IN () +f4 BETWEEN f2 AND -1e-10 +f2 NOT +f4 IN (,) +f1 BETWEEN 'O''Reilly' AND -10 +f2 ILIKE "\t" +f2 BETWEEN "xyz%" AND f1 +f3 IN (-10, -.5, 10, 0.0000000001) +f5 IN () +f5 NOT +NOT +(f1 IN ("xyz%", -1)) OR ((hasAll(f1, 1)) OR ((f6 BETWEEN "xyz%" AND 10) OR ((f3 <= ' ') AND (f2 = "\t")))) +((f1 > f3) AND ((f3 BETWEEN "abc" AND 0) AND (f3 < "\n"))) OR (f5 BETWEEN "xyz%" AND "hello world") +f2 IN [1 2 3] +f4 >= "abc" +f6 IN (1, -1) +f6 IN (1e10, -3.5e-2) +f2 IN (,) +f4 IN (f2, 0.0000000001, 0.0000000001) +hasAll(f5, 999999999999) +hasAll(f6, 3.14) +f5 === f2 +hasToken(f2, -1e-10) +f5 <= '' +hasAll(f6, "\t") +f6 IS NULL +f2 BETWEEN f2 AND -2.7 +f6 BETWEEN AND f6 +(f3 <= 10) OR (f2 IN (1, "\t", "xyz%", "xyz%", f4)) +hasAll(f2) +has(f4, 0) +f1 BETWEEN -1e-10 AND "\t" +f5 BETWEEN -10 AND "\t" +(f4 BETWEEN 3.14 AND "\t") OR ((f4 IN (1)) OR ((f3 BETWEEN 1e10 AND f2) OR (f3 ILIKE "abc"))) +f6 <> 0 +f6 <= 3.14 +hasAny(f2, 3.14) +has(f1, f5) +(f6 REGEXP 'test') AND (has(f4, 'test')) +(f4 >= -.5) AND (f3 BETWEEN "\n" AND "xyz%") +has(f4) +f5 ILIKE "xyz%" +has(f1, 10) +hasAll(f1, f4) +has(f6, -2.7) +f1 NOT REGEXP "%wild%" +(f6 NOT REGEXP '') OR (f3 NOT CONTAINS "\n") +f6 BETWEEN 0.0000000001 AND "\t" +(((f5 NOT LIKE "abc") AND ((f5 < "\t") OR (f2 <> -1))) AND (f3 IN (-1e-10, "xyz%"))) AND (f3 != 1) +f1 IS NULL +(f3 NOT LIKE "%wild%") AND (hasAll(f2, 3.14)) +f6 BETWEEN -1e-10 AND 10 +f5 IN (3.14, f4) +f2 = f6) +(f1 = 0.0000000001 +(f6 = f2 +f5 == f3 +f4 = 'unclosed +f4 NOT +(f3 >= "abc") OR (hasToken(f3, "%wild%")) +f3 ILIKE "a_b%" +f4 CONTAINS '' +hasToken(f6, f4) +f2 = NULL +f1 < 1 +hasAll(f3) +f4 IN (6.02e23, "%wild%", TRUE, -10) +f6 IN (' ', 0.0000000001, 0.0000000001, f6, 'test') +f6 BETWEEN 1 AND f1 +(f3 <= "\t") AND ((f5 = ' ') AND ((f3 BETWEEN -1e-10 AND "hello world") AND (f2 NOT REGEXP "%wild%"))) +(f4 NOT ILIKE 'O''Reilly') OR (f3 BETWEEN "xyz%" AND -2.7) +f4 ILIKE "xyz%" +f4 = 'unclosed +hasAll(f2, f4) +(f5 BETWEEN "%wild%" AND -2.7) OR ((has(f6, 'O''Reilly')) OR (f5 IN ("\n", -.5))) +f2 BETWEEN -2.7 AND 1e10 +f2 >= "\t" +f2 = f3) +f2 BETWEEN 'O''Reilly' AND "\t" +f4 IS NULL +f6 IN ('O''Reilly', "a_b%") +f1 NOT REGEXP "abc" +f4 IN ("%wild%", -1, f5) +f5 IN ('O''Reilly', .5, 0.0000000001, "abc") +f4 LIKE ' ' +f2 BETWEEN ' ' AND 'test' +(f3 IN ('test', .5)) OR (f6 <> '') +f3 LIKE +has(f6, -1) +f6 IN (0.0000000001, 3.14, -3.5e-2, f6) +f2 IN () +f4 NOT LIKE "a_b%" +hasAny(f3, 0) +f1 = +() +f2 <> f3 +f2 IN ("a_b%", f2, -.5, 0) +f2 OR OR -1 +f6 == f6 +(f1 > -1e-10) OR (hasToken(f2, f4)) +((f2 = f3) AND (f5 == f1)) AND (f2 BETWEEN f3 AND "\n") +f4 IS NULL +(f6 ILIKE "\n") AND ((f2 IN (TRUE, 999999999999, "%wild%", "abc", 'O''Reilly')) AND ((f5 BETWEEN f5 AND "hello world") OR (f5 IN (10)))) +(f3 BETWEEN '' AND f6) OR (f2 NOT ILIKE 'O''Reilly') +f6 BETWEEN -2.7 AND f1 +hasAll(f1, "a_b%") +f5 IN [1 2 3] +f6 < -10 +hasAny(f3, -1) +has(f5, "hello world") +f5 IN (-10) +hasToken(f1, -3.5e-2) +f5 IN (f4, "hello world", "xyz%") +f5 != TRUE +hasAll(f4, 1) +f4 IN [1 2 3] +f3 OR OR -.5 +() +f4 BETWEEN 6.02e23 AND "hello world" +f5 IN (f4, -1, 0) +f1 IN (-1, "\t", f2, -.5, 'O''Reilly') +f1 NOT REGEXP ' ' +f3 BETWEEN f4 AND f5 +f2 LIKE "\t" +f4 IN (1, -2.7) +NOT +f2 IN (6.02e23) +f3 >= ' ' +f4 BETWEEN 1e10 AND f6 +f4 = f6) +f5 NOT REGEXP "abc" +f4 REGEXP '' +f6 BETWEEN "\t" AND 'O''Reilly' +f5 OR OR 10 +(f2 IN ('test', f2)) OR (f5 BETWEEN "xyz%" AND f6) +f1 REGEXP "xyz%" +f3 IN ("%wild%") +f2 NOT ILIKE ' ' +has(f3, -10) +has(f4, -1e-10) +f3 NOT REGEXP "a_b%" +f2 BETWEEN -2.7 AND -1e-10 +(f1 IN (f3, f3, 3.14, 3.14, 0)) AND ((f4 NOT CONTAINS 'O''Reilly') AND (f1 REGEXP '')) +f5 > 6.02e23 +f2 IN ("hello world", f4, "xyz%", 999999999999) +f3 IS NULL +(has(f6, 'test')) AND (f5 <= "a_b%") +f6 >= -1 +f3 <> -10 +f1 NOT REGEXP "a_b%" +f3 LIKE "hello world" +f5 === '' +f2 = f5 +f2 NOT LIKE '' +f3 BETWEEN 'O''Reilly' AND -2.7 +f6 BETWEEN "xyz%" AND "a_b%" +f2 BETWEEN 1e10 AND '' +f6 != 'test' +f1 NOT LIKE "%wild%" +f6 = f6) +f6 LIKE "hello world" +f3 LIKE "hello world" +f1 <= 0 +f4 = +(f2 BETWEEN '' AND "\t") OR (f6 <= 1e10) +hasToken(f6, -1) +f3 = NULL +f6 BETWEEN "\n" AND f6 +f6 IN ('', -3.5e-2, "\n") +f6 BETWEEN "abc" AND "%wild%" +f2 ILIKE 'O''Reilly' +f6 IN ("abc", -1, "\t") +f1 === 0 +f5 >= -1 +f4 OR OR "\t" +f5 != "xyz%" +f5 = 'unclosed +f2 IN (,) +f1 IN (f3) +() +f5 NOT +f4 <> -10 +f4 >= -2.7 +f4 BETWEEN "abc" AND "%wild%" +(f6 IN (10, 6.02e23)) OR (f2 != 999999999999) +f6 NOT LIKE 'O''Reilly' +f6 BETWEEN 'test' AND "%wild%" +f3 BETWEEN 6.02e23 AND -3.5e-2 +f2 <> -.5 +f1 NOT +f1 IN (f5, -2.7, 0.0000000001, "%wild%", 0.0000000001) +f5 = "hello world") +f3 LIKE '' +() +(f5 = "\t") AND (f4 BETWEEN 10 AND -.5) +f1 BETWEEN "%wild%" AND f1 +(f1 NOT CONTAINS 'test') OR (f5 BETWEEN 6.02e23 AND 'test') +hasToken(f6, f2) +f2 === -1e-10 +f5 BETWEEN 0.0000000001 AND -1e-10 +f2 > 1e10 +f3 BETWEEN 1 AND "\n" +(hasAll(f6, -1)) AND (f3 IN (-3.5e-2)) +f2 BETWEEN 6.02e23 AND "xyz%" +f4 === 0.0000000001 +hasAny(f6, -1e-10) +f3 NOT LIKE "a_b%" +f3 BETWEEN AND 0.0000000001 +(f5 IN ('', ' ', 10, f6)) AND (f4 IN (0.0000000001, 1e10)) +(f1 >= 0) OR (f5 <= 'O''Reilly') +f4 IN ('test', f3, ' ', -3.5e-2) +f3 <> '' +f4 BETWEEN -.5 +f6 === -2.7 +f2 <= TRUE +(f6 < 10) OR (hasAll(f4, f4)) +f4 BETWEEN "hello world" AND -3.5e-2 +f3 = -.5) +f2 IN (10, 999999999999, f4) +f6 BETWEEN "hello world" AND -1e-10 +f1 === TRUE +f2 IN ("hello world", f2, 'test', f2) +f4 >= "\n" +f5 BETWEEN TRUE AND 0.0000000001 +(f4 == -1) AND ((f2 < 'test') AND (f4 IN ('O''Reilly', -.5, -2.7, .5, f6))) +((f3 < -10) AND ((f4 != -1e-10) AND ((f5 != 1) OR (f5 <> 1)))) OR (f1 IN (3.14, -.5, -10)) +f6 < 999999999999 +f3 IN (999999999999) +(hasAll(f4, -1)) OR (f1 CONTAINS ' ') +(f3 = -10 +f1 IN (-.5, f3, f4) +f1 BETWEEN "%wild%" AND 0.0000000001 +f1 > .5 +(f6 BETWEEN f4 AND -2.7) OR (f3 BETWEEN -1e-10 AND "xyz%") +f1 CONTAINS "a_b%" +f6 IN (1e10, "a_b%", ' ', 999999999999) +(f2 NOT CONTAINS "xyz%") AND (f1 BETWEEN f2 AND 6.02e23) +f5 < "a_b%" +hasToken(f6) +f4 = +f5 IN (,) +f6 IN ("xyz%", 6.02e23, ' ', "xyz%", -1e-10) +f1 IN ("\t", f1) +f4 BETWEEN "abc" AND f1 +f4 REGEXP '' +f1 > -10 +f2 = NULL +hasToken(f2, f3) +f1 >= 0 +f3 BETWEEN 10 AND f6 +f6 BETWEEN ' ' AND 3.14 +f2 BETWEEN -2.7 AND ' ' +f1 BETWEEN 1e10 AND "abc" +f4 LIKE +f6 CONTAINS "abc" +f5 = +f6 BETWEEN f4 AND "\t" +(f2 = ' ' +f6 NOT CONTAINS "%wild%" +(f1 IN (1, -1, "%wild%", 10, 10)) AND (f4 != '') +(hasAll(f1, '')) OR (f3 IN ('test')) +f2 === TRUE +has(f5, f2) +has(f1, 'O''Reilly') +hasToken(f2, f5) +hasAny(f1, -.5) +f1 BETWEEN .5 +(f3 <> f5) OR (((f4 IN (-.5, 1e10, "a_b%")) AND ((f2 <= -1) AND (f5 = 999999999999))) OR (f3 CONTAINS ' ')) +f4 = NULL +hasToken(f2, 0) +f1 BETWEEN f3 AND -1 +AND f6 = 999999999999 +f1 IN [1 2 3] +f6 NOT REGEXP "xyz%" +f4 NOT +hasToken(f3, -10) +(f6 BETWEEN -3.5e-2 AND f3) OR (f4 BETWEEN -2.7 AND 1) +(f6 BETWEEN "\n" AND 1) AND (hasAll(f6, TRUE)) +f5 IN (3.14, "a_b%") +hasAll(f4, 999999999999) +() +f4 NOT LIKE "\t" +f2 IN (f2, 0.0000000001) +(f5 = f6) OR (f3 NOT ILIKE ' ') +f4 NOT ILIKE "xyz%" +f6 BETWEEN "xyz%" AND 1e10 +f1 != 'test' +f2 IN () +f2 = +f3 NOT CONTAINS '' +(f6 BETWEEN '' AND TRUE) OR (hasAny(f2, "%wild%")) +((f6 IN ("%wild%", 1, 0.0000000001)) OR (f5 BETWEEN -1e-10 AND "xyz%")) OR (f5 LIKE '') +f3 IN ("\t", -1, -.5, "\t", "%wild%") +f2 LIKE +f6 IN (f5, f1, 1, -10, ' ') +(f2 BETWEEN "hello world" AND "a_b%") AND (f5 BETWEEN 6.02e23 AND "xyz%") +f5 <> -1e-10 +f5 IN (,) +f1 BETWEEN -1e-10 AND ' ' +f1 = +f2 IN (f4, "\n", 10) +f4 NOT ILIKE 'test' +f1 = 3.14) +f5 BETWEEN "a_b%" AND f1 +f6 < f6 +NOT +f6 NOT REGEXP ' ' +f4 == "xyz%" +f4 BETWEEN AND 1 +(f3 BETWEEN TRUE AND f6) AND ((hasToken(f6, -.5)) OR (hasToken(f2, f4))) +f3 IN (-.5) +f1 REGEXP "a_b%" +f4 BETWEEN "hello world" AND 0 +f5 = NULL +f4 BETWEEN '' AND "xyz%" +f1 IN ('O''Reilly', f6) +f5 === '' +f5 < f2 +f3 IN ("\t", ' ', f6, -1e-10) +(f5 >= "\n") AND (f1 REGEXP 'test') +f6 IN (f2, -10) +() +f2 IN (10, 6.02e23, "xyz%") +f5 NOT +f2 IS NULL +f4 BETWEEN 3.14 AND "%wild%" +hasToken(f1) +(f1 BETWEEN 'test' AND 1) AND (f5 == "\t") +(f2 REGEXP "\t") OR (hasToken(f6, f4)) +AND f4 = -10 +f6 CONTAINS ' ' +(f4 >= 3.14) OR (hasAll(f6, f3)) +f4 IN ('', 1e10, -1e-10, "a_b%", 6.02e23) +f1 <> 'test' +f2 LIKE +f3 IN () +f1 = 'unclosed +f3 NOT REGEXP 'test' +f4 BETWEEN 'test' AND -3.5e-2 +f2 > "abc" +(((f1 != "%wild%") OR ((f4 > -10) OR (f5 > f2))) OR (f2 == f4)) AND (f4 IN (-.5, '', -1e-10, 1e10, -1)) +f2 IN ('O''Reilly', '', "\n") +f2 = "abc") +f6 IN ('O''Reilly', f6, .5) +f2 ILIKE "%wild%" +AND f2 = 1 +f1 = NULL +f1 BETWEEN 999999999999 AND f6 +f1 != 1 +f6 BETWEEN f6 AND -1e-10 +has(f1, '') +(f6 = f1 +f1 BETWEEN 'O''Reilly' AND 1 +f6 IN (TRUE, 1, 3.14) +f2 NOT +f6 == .5 +f3 IN () +f1 IN (TRUE, "a_b%", -.5, 1) +f3 LIKE +f3 = 'unclosed +hasAny(f4, -.5) +has(f4, .5) +f2 IN (' ') +f4 IN ('O''Reilly', 1) +f3 LIKE +hasAny(f6, -3.5e-2) +f3 >= 'test' +(f3 BETWEEN -1e-10 AND -2.7) AND (f5 BETWEEN -10 AND "\t") +NOT +NOT +f3 BETWEEN "a_b%" AND f4 +f5 BETWEEN -1 AND -1e-10 +f4 BETWEEN f4 AND 999999999999 +f2 NOT +hasAny(f1, "xyz%") +has(f6) +(f4 = 0) AND (f5 IN ('O''Reilly', -2.7, -.5)) +(has(f1, 'test')) OR (f3 BETWEEN "a_b%" AND "abc") +f2 NOT REGEXP "a_b%" +f3 IN (6.02e23) +((has(f4, -10)) AND (hasAny(f2, .5))) OR (f5 BETWEEN "xyz%" AND -2.7) +f1 NOT LIKE "xyz%" +f3 = -.5) +f2 IN () +f2 BETWEEN 1 AND .5 +hasAll(f4, '') +f6 NOT CONTAINS 'test' +hasAny(f6, "\n") +f3 <> 'O''Reilly' +f2 BETWEEN f6 AND 1 +f6 IN (10, "a_b%", -1e-10, f6, 0) +has(f3, "hello world") +(f5 NOT ILIKE "%wild%") AND (f4 IN (1e10, 3.14, 1, f6)) +f3 NOT LIKE "abc" +() +f6 IN (-3.5e-2, f1, "xyz%") +f2 IN (-2.7, f4) +hasAny(f3, "xyz%") +(f4 = f3 +f6 <> "xyz%" +f6 IN (f5, .5, "abc") +f4 IN ('', 0.0000000001) +f2 < -10 +f1 == 999999999999 +f3 REGEXP "hello world" +f4 BETWEEN f4 AND f6 +(f3 IN (-.5, -.5, f4, f4, 1)) AND (f1 REGEXP "\n") +hasToken(f6, f2) +f4 IN (f1, 'O''Reilly', -10, -10, 999999999999) +f6 CONTAINS 'O''Reilly' +f4 ILIKE ' ' +f2 BETWEEN TRUE AND f6 +f4 == -2.7 +f4 IN (10, ' ', 1e10) +f1 != f4 +() +f2 IN (.5, TRUE) +f1 = NULL +hasAll(f5, -10) +f1 LIKE +f5 IN (0, 1e10, "\t", 'O''Reilly') +f2 REGEXP "a_b%" +f5 OR OR "hello world" +f5 NOT +f6 BETWEEN f5 AND "xyz%" +(f5 = 999999999999 +f3 < "\t" +f6 IS NULL +f4 IN ('test', TRUE, f2, 'test') +f4 BETWEEN "abc" AND "abc" +((hasAll(f1, TRUE)) OR (f3 IN (999999999999))) OR (f6 IN ("%wild%", f2, f3)) +f6 >= 10 +f6 LIKE 'O''Reilly' +f3 IN (1, 1) +f1 BETWEEN "xyz%" AND -2.7 +f3 BETWEEN 1e10 AND -2.7 +hasAll(f6, "\n") +f3 BETWEEN 10 AND '' +(f3 BETWEEN -2.7 AND f5) OR (f6 IN (' ', f1, "a_b%", 6.02e23, "\t")) +f6 BETWEEN "hello world" AND 999999999999 +f5 BETWEEN -.5 AND 6.02e23 +f1 BETWEEN "hello world" AND -10 +f5 IN (-1, f3, -3.5e-2, "\n") +(((hasToken(f4, 10)) OR (f2 NOT CONTAINS "hello world")) OR (f1 <> -3.5e-2)) AND ((f4 IN (-1e-10, 1e10, 10, '', 0)) OR ((f4 <= .5) OR (hasAll(f5, "hello world")))) +f2 BETWEEN "\t" AND "xyz%" +f1 IN (1) +f6 = 10) +f1 BETWEEN '' AND .5 +f3 NOT LIKE "\n" +hasToken(f2, 1) +f3 NOT ILIKE "xyz%" +f6 = 'unclosed +(f4 ILIKE "hello world") AND (hasAll(f4, "%wild%")) +f5 = NULL +f6 BETWEEN AND f4 +f6 BETWEEN 6.02e23 AND 999999999999 +f2 IN (999999999999, f3) +f3 < 1 +f6 BETWEEN 1e10 AND "xyz%" +(f5 REGEXP "%wild%") AND (f1 IN (TRUE, -2.7)) +(f6 IN (-.5, f3, "abc")) AND ((has(f4, -.5)) AND (f6 IN (-10, "\n", -1, 'O''Reilly'))) +f1 LIKE "hello world" +f4 IS NULL +f2 === -1 +f2 REGEXP "%wild%" +f4 IN ('test') +f1 IN (-1) +f3 IN () +(f5 = 1e10 +f2 BETWEEN -.5 AND 6.02e23 +(f2 = 3.14 +f2 ILIKE "\t" +f5 IN ("abc", f1, f1, 0, f5) +f2 BETWEEN 'test' AND 10 +(f5 NOT ILIKE "%wild%") AND (f6 IN (1e10, "\n", TRUE, "\t", "\n")) +f5 = f4 +f3 NOT ILIKE "abc" +f2 BETWEEN f5 AND '' +f6 BETWEEN TRUE AND 1 +f5 != -1 +f4 BETWEEN 3.14 AND "xyz%" +f2 >= 'test' +f4 BETWEEN 0.0000000001 AND 1e10 +((hasAll(f5, "a_b%")) OR (f4 BETWEEN TRUE AND -.5)) AND ((f4 BETWEEN f6 AND 999999999999) AND (f1 IN (1e10, f6, 3.14, ' '))) +f6 <= -10 +f5 = 10 +hasAny(f2, "\t") +f2 REGEXP 'O''Reilly' +f2 BETWEEN f4 AND '' +f4 BETWEEN f5 AND 0.0000000001 +AND f6 = 1e10 +f4 <> 1e10 +f3 NOT REGEXP "xyz%" +(f6 IN (f1)) AND (hasAny(f4, 'test')) +f3 BETWEEN f1 +f6 = 'unclosed +f2 BETWEEN .5 +f1 OR OR "\n" +has(f5, "\n") +f4 IN (,) +has(f3, 'test') +((f6 BETWEEN "xyz%" AND 6.02e23) OR (hasToken(f1, 6.02e23))) OR (f1 BETWEEN f5 AND f3) +f6 IN (6.02e23, 1e10, 6.02e23) +f6 BETWEEN 'O''Reilly' AND -.5 +f1 = 'unclosed +f1 IN (1e10) +f5 IN ("a_b%", 0) +f2 BETWEEN -3.5e-2 AND f4 +f3 IN ("a_b%", 3.14) +(f3 = -10 +f3 BETWEEN "xyz%" AND f2 +(f2 IN (f6, -3.5e-2)) OR (f1 IN (TRUE, 6.02e23, 6.02e23, -1)) +hasAll(f2, "%wild%") +f6 IN [1 2 3] +f1 IN (,) +(f1 IN (999999999999, f1)) AND (f4 BETWEEN f3 AND 'test') +f1 IS NULL +f6 NOT ILIKE "%wild%" +f4 BETWEEN "xyz%" AND f4 +hasAny(f3, -10) +f5 REGEXP "abc" +f5 NOT CONTAINS "hello world" +f4 IN () +f6 <= f2 +(f6 IN ("\n", "xyz%", f5, "xyz%", 0)) AND (hasAll(f2, ' ')) +f4 > "a_b%" +has(f3) +f2 CONTAINS ' ' +f4 IN (10, 'test', 3.14, "xyz%") +(hasToken(f3, -1e-10)) OR ((f2 > "a_b%") AND (hasAny(f5, -1e-10))) +f2 BETWEEN f1 AND f5 +f2 IN ('', -2.7, f3) +f4 BETWEEN f6 +(f4 BETWEEN TRUE AND -1e-10) OR (f2 NOT CONTAINS "xyz%") +f4 > 999999999999 +NOT +f2 NOT +hasAll(f2, 0.0000000001) +has(f6, "xyz%") +f3 < 'O''Reilly' +f5 <= f3 +f2 OR OR -1 +(f6 NOT CONTAINS "abc") AND (f3 IN (f1, "\n", 0.0000000001, 10)) +f6 LIKE +f1 IN (-3.5e-2, 0.0000000001, "hello world", TRUE) +(f1 IN (f5)) AND (hasAny(f2, -.5)) +f3 BETWEEN 3.14 AND 999999999999 +f2 > -1 +(f4 IN (f2, 1e10, 10)) OR (f3 IN ("abc", f4)) +(f3 >= ' ') OR ((f4 IN ('test', 'test', -.5, "\n", 0.0000000001)) AND (f2 BETWEEN 'O''Reilly' AND TRUE)) +hasAny(f1, 'test') +f5 != 'test' +f1 <= 10 +f6 IN (-1, -1e-10, '') +hasAll(f2, .5) +() +f1 BETWEEN -3.5e-2 AND 3.14 +NOT +f6 = 10 +f5 BETWEEN "abc" AND "\t" +f6 = ' ') +(f5 <> ' ') OR (f6 ILIKE '') +hasToken(f5, -3.5e-2) +f6 != 0.0000000001 +f5 IN (10, 0.0000000001) +(hasAll(f1, "a_b%")) AND (has(f1, -1e-10)) +f4 IN ("\n", "abc") +f5 BETWEEN 3.14 AND 1 +(f2 BETWEEN 999999999999 AND 1e10) OR (f3 NOT ILIKE 'O''Reilly') +f2 < "\n" +hasAny(f5, "abc") +f3 IN (,) +f5 IN (f5, 6.02e23, 0) +f3 == f3 +f6 BETWEEN -.5 AND -3.5e-2 +(f3 LIKE "hello world") OR (f6 IN ("abc", -10, 1, 999999999999, 1e10)) +f1 IN (0.0000000001) +f5 BETWEEN -10 AND 10 +f5 = 6.02e23) +f6 = f5 +(f4 = -2.7 +hasAny(f5, .5) +f4 IN [1 2 3] +f3 BETWEEN "\t" +f1 = f2) +f4 = NULL +hasAll(f3, f5) +f3 == TRUE +f1 IN [1 2 3] +f5 IN ("%wild%", TRUE) +f3 != f1 +hasAll(f4, -1) +f5 === "\t" +f6 < -2.7 +f3 LIKE "\t" +f6 != 1e10 +f2 <> 0 +(f3 IN (-.5, f5, "\n", "abc")) OR ((f5 IN ("xyz%", f4, 3.14, "a_b%")) AND (f6 BETWEEN "%wild%" AND "hello world")) +hasAny(f5, "a_b%") +(f5 IN (-10, f1, ' ')) AND (f6 IN ("%wild%", 6.02e23, -.5, f4, "a_b%")) +f5 = -10 +f6 REGEXP "\n" +f1 == 'test' +f4 IN (,) +f5 IN (-2.7, f2, "hello world", 10) +(hasAll(f4, 999999999999)) OR ((f5 IN (-3.5e-2, 3.14, 1)) OR (f1 BETWEEN f6 AND 'test')) +f5 BETWEEN -1 +(f1 IN (.5, 10, 999999999999)) OR (f4 IN (0, f2, "\n", 1)) +f6 === f1 +f3 BETWEEN AND 999999999999 +hasAny(f5, -.5) +(f4 IN ("xyz%", 1, f1, 1, 1e10)) OR (f1 LIKE "abc") +f3 BETWEEN -10 AND -.5 +hasAll(f3, "\n") +f4 IN ("xyz%", "xyz%") +f5 IN [1 2 3] +f6 BETWEEN f4 AND f5 +f1 = 'unclosed +f2 IN ("hello world", 3.14) +f1 NOT CONTAINS "xyz%" +hasToken(f4, "abc") +has(f4, f6) +f6 BETWEEN 1e10 AND 1e10 +hasAny(f2, 10) +hasAny(f3, "a_b%") +f5 NOT LIKE "%wild%" +f5 = +(hasAny(f3, -.5)) OR (f4 = .5) +((f4 BETWEEN f5 AND "hello world") AND (f1 IN (10, "%wild%"))) OR (hasAll(f2, 3.14)) +f2 BETWEEN ' ' AND .5 +hasAny(f2, 10) +f2 IN (f5, TRUE, '', 'test', "\t") +f4 >= 0 +f2 LIKE '' +f2 < 0 +(f3 NOT REGEXP "\t") AND (f2 ILIKE "xyz%") +() +f5 NOT +(f6 IN (f2, 6.02e23, ' ', 'test', f3)) AND ((f4 IN ('O''Reilly', -.5, -2.7)) OR (f4 IN ("xyz%", 0))) +f4 = NULL +f3 > -3.5e-2 +(hasToken(f5, 'O''Reilly')) OR (f1 IN (0.0000000001, 1, "xyz%", "abc", 3.14)) +hasAny(f3, -10) +hasAny(f1, f1) +hasAll(f4, "xyz%") +hasToken(f2, f5) +f6 BETWEEN f2 AND .5 +NOT +f1 IN ('test', f4) +f4 BETWEEN 6.02e23 AND -1 +f5 >= ' ' +f1 LIKE "%wild%" +f5 BETWEEN f5 AND f3 +f2 IN (0) +hasToken(f2, '') +hasToken(f2, 0.0000000001) +f5 < '' +() +(f3 BETWEEN -.5 AND 0) OR (f2 IN ("a_b%", "%wild%")) +f2 IN ('test', f3) +hasAny(f4, 0) +hasAny(f6, 1e10) +f2 IN (1) +f6 >= 0.0000000001 +f2 BETWEEN -.5 AND 'test' +f6 >= -3.5e-2 +f5 NOT ILIKE "\n" +f1 ILIKE "xyz%" +f2 IN [1 2 3] +f6 IS NULL +has(f6, ' ') +has(f2, 3.14) +f5 BETWEEN AND f3 +f5 IS NULL +f6 != 0.0000000001 +f3 BETWEEN f4 AND '' +f1 <> "\n" +f5 IN () +hasAll(f3, f6) +f2 LIKE "hello world" +hasAny(f1, -1e-10) +f2 NOT REGEXP "xyz%" +has(f6, 999999999999) +f6 OR OR 0.0000000001 +f6 REGEXP '' +f6 BETWEEN ' ' AND -3.5e-2 +hasToken(f2) +hasAll(f4) +(f2 BETWEEN f2 AND -3.5e-2) OR (f4 NOT CONTAINS "%wild%") +f2 BETWEEN TRUE AND f4 +f3 NOT CONTAINS "\n" +f3 IN (6.02e23, 'test') +f6 BETWEEN -1 AND '' +f3 ILIKE "a_b%" +f5 LIKE "%wild%" +f1 CONTAINS "abc" +f3 BETWEEN f6 AND f5 +has(f2) +f5 = +f6 < 999999999999 +hasAny(f4) +f3 IN (f2, '', f2, f4, "xyz%") +(f6 BETWEEN 10 AND 'O''Reilly') AND (f5 ILIKE ' ') +hasToken(f3, "a_b%") +(hasToken(f6, f4)) AND (f2 IN (f6)) +hasToken(f1, f2) +hasAll(f3, -2.7) +hasAny(f2, 'test') +f6 != f3 +f4 BETWEEN -2.7 AND 1 +f6 IS NULL +f2 BETWEEN -1 AND "abc" +f6 BETWEEN 1 AND -10 +f3 NOT LIKE "\n" +(f2 = "a_b%" +(f6 BETWEEN "hello world" AND -1) AND (f4 == -1e-10) +() +(f2 NOT LIKE "%wild%") AND (f3 ILIKE ' ') +f1 = 1 +NOT +f6 IN ("\n", -10, 0, "\n") +hasAny(f3, f3) +f1 LIKE "xyz%" +f2 BETWEEN -.5 AND .5 +f5 NOT ILIKE "\n" +f6 = .5 +f1 = f3) +f1 LIKE "xyz%" +f1 IN (f1) +f5 BETWEEN 6.02e23 AND 6.02e23 +hasAll(f4, -.5) +f5 IN () +f4 BETWEEN AND 1 +f6 IS NULL +f5 = 'unclosed +f2 IN (.5, f3) +f5 IN (f5, .5, -1e-10, 6.02e23) +f4 == 999999999999 +(f2 == TRUE) OR (f6 NOT CONTAINS 'O''Reilly') +f2 BETWEEN f6 AND f5 +f2 LIKE "abc" +f3 === -1 +f6 != f1 +f3 NOT +hasToken(f3) +(f3 <> "xyz%") AND (f5 NOT ILIKE "\t") +f5 NOT +f5 NOT +f6 BETWEEN 1 AND 1 +f3 <> "\n" +hasAll(f3, -1e-10) +hasAny(f2, -.5) +AND f3 = "abc" +() +f2 BETWEEN f3 AND "\t" +hasAll(f1, f3) +f1 < 3.14 +f3 == "\n" +() +(f1 IN (-10, "\n", -1e-10, f2, '')) AND (f4 IN (-.5, .5, '', -10, -1)) +has(f5, 10) +hasAll(f3, 0.0000000001) +f4 BETWEEN f5 AND 0.0000000001 +f6 <= "%wild%" +f2 IN (TRUE, -1e-10, 0, 1e10) +f2 BETWEEN "\t" AND f2 +f6 BETWEEN -2.7 AND 3.14 +f6 = f3 +hasToken(f5, 3.14) +f2 IN ('test', -.5, 0.0000000001, 1) +f6 IN () +f6 IN ("a_b%", 'O''Reilly') +NOT +f4 OR OR 1e10 +f6 < 6.02e23 +f1 < "\t" +f4 NOT REGEXP 'O''Reilly' +has(f6, ' ') +hasToken(f5, "\n") +AND f6 = 1e10 +(f5 BETWEEN "%wild%" AND 1e10) OR (f6 NOT ILIKE ' ') +f4 BETWEEN AND 999999999999 +f6 == -10 +f4 <> 999999999999 +f4 BETWEEN 6.02e23 AND 'test' +has(f6) +has(f4, f3) +f2 BETWEEN 10 +(f5 BETWEEN f1 AND "hello world") AND ((f4 == 6.02e23) AND ((hasToken(f2, f4)) OR (f6 BETWEEN f4 AND f4))) +f2 IN (f2, -.5, 10, f2) +NOT +f3 IN (TRUE) +hasAny(f2, -2.7) +AND f1 = "a_b%" +f4 = "abc" +f4 IN () +f4 IN (-1e-10) +f4 <> f6 +f1 = +f3 IN (f5, f4, 'test', .5) +f4 IN (-2.7, f6, 999999999999) +f2 BETWEEN 1e10 AND '' +f6 BETWEEN f1 AND -1e-10 +f6 NOT REGEXP ' ' +f4 LIKE ' ' +f2 IN [1 2 3] +f1 IN ('', 6.02e23, f1, 3.14, 10) +hasAll(f4, ' ') +f6 IN () +f1 IN (-1e-10, f1, 'test') +f3 BETWEEN "\n" AND 10 +f5 != 0 +f4 IN [1 2 3] +hasAny(f5, -2.7) +f5 IS NULL +f4 LIKE "xyz%" +hasToken(f4, "a_b%") +f4 BETWEEN -.5 AND 'O''Reilly' +f3 != 0.0000000001 +f4 <= "%wild%" +f1 IN [1 2 3] +f3 BETWEEN -.5 AND f5 +f2 NOT REGEXP "a_b%" +f4 NOT REGEXP "xyz%" +hasAll(f2, f1) +f4 LIKE +f3 BETWEEN -1e-10 +f3 BETWEEN "\t" AND 0 +f5 IN (f6, f2, "a_b%", f2) +(f6 NOT REGEXP 'O''Reilly') OR (has(f2, f6)) +f4 = NULL +f3 = 999999999999 +f3 = "a_b%") +f6 NOT ILIKE '' +f3 IN (f5, 3.14, 0.0000000001, 'O''Reilly') +f6 IN ("abc", "xyz%") +f1 OR OR f2 +f6 BETWEEN -1 AND TRUE +f6 NOT +f1 < -1e-10 +f6 == 1e10 +f1 = NULL +f4 <> -.5 +f1 OR OR "\t" +hasToken(f3, f6) +f4 IN [1 2 3] +f3 == f3 +f5 LIKE +((f4 BETWEEN "\t" AND "\t") OR (hasAll(f6, 6.02e23))) AND (f2 != "\n") +f3 NOT +f1 IN [1 2 3] +f4 IN (10, "abc", TRUE, -10) +() +f2 >= TRUE +f1 BETWEEN -1e-10 AND "%wild%" +f4 IN ("hello world") +f3 <> f6 +(hasAll(f6, "abc")) AND (f1 BETWEEN -1 AND "%wild%") +f3 = 1 +f3 = NULL +f1 BETWEEN 999999999999 AND 'O''Reilly' +f2 === 'test' +AND f4 = 'test' +f5 IN (3.14, "%wild%") +f4 NOT +(f5 IN (3.14, TRUE, "a_b%", 3.14)) OR (f2 NOT CONTAINS "xyz%") +f1 CONTAINS "a_b%" +f5 IS NULL +(f2 == -1e-10) AND (f4 NOT CONTAINS "abc") +f6 LIKE "abc" +(hasAny(f4, "hello world")) OR (f2 IN (f6, 'test', -1e-10, 0)) +f2 >= f6 +f4 < f1 +f3 === TRUE +(f2 BETWEEN "\n" AND "\t") OR (f6 <> f4) +(hasAll(f3, "a_b%")) OR ((f3 NOT ILIKE "abc") AND (f6 IN ('test', 999999999999, 1, f6))) +f2 BETWEEN 1e10 AND ' ' +hasAny(f3) +f6 IN ("hello world") +f3 === 0.0000000001 +(f4 CONTAINS "abc") AND (has(f2, f4)) +f1 IN [1 2 3] +hasToken(f4, "abc") +(hasAny(f2, f6)) AND ((hasToken(f4, f6)) AND (hasToken(f6, 6.02e23))) +f4 <> f1 +hasAny(f1) +f2 NOT REGEXP "hello world" +has(f5, "\n") +() +f6 BETWEEN 6.02e23 AND TRUE +f2 NOT LIKE "abc" +f4 NOT CONTAINS '' +f4 = "xyz%") +hasAny(f5, 999999999999) +f6 BETWEEN f5 AND "hello world" +f2 REGEXP "hello world" +f6 <> "abc" +hasToken(f6, f5) +f1 IN ("hello world", .5, TRUE, "\t", 3.14) +has(f3, "\t") +f4 IN ("xyz%", f3) +f5 BETWEEN ' ' AND f4 +f4 OR OR 'O''Reilly' +f1 <= f1 +f6 IN (,) +f6 NOT ILIKE "a_b%" +f3 REGEXP 'test' +f3 IN [1 2 3] +f4 ILIKE "hello world" +(f4 IN ("a_b%")) AND (f1 BETWEEN 0 AND 6.02e23) +f4 BETWEEN '' AND 10 +f4 OR OR "a_b%" +has(f4, .5) +(f5 = 10 +has(f6, ' ') +f5 IN (-3.5e-2, 6.02e23) +f3 LIKE +hasAll(f5, "hello world") +() +f5 NOT REGEXP "\n" +f4 = 999999999999) +f6 IS NULL +NOT +hasToken(f5, -.5) +f1 BETWEEN 1e10 AND 'test' +f1 BETWEEN f6 AND ' ' +f1 NOT ILIKE "xyz%" +f6 BETWEEN TRUE AND 10 +(f6 IN (TRUE, "a_b%", "xyz%", -10, "hello world")) OR (f5 <> f1) +f4 IN (1e10) +(f6 = -1 +f1 BETWEEN 10 AND '' +f5 < "hello world" +f3 = -2.7 +(f3 BETWEEN 10 AND TRUE) OR ((f4 LIKE "a_b%") AND (f5 IN (f6))) +f6 IN [1 2 3] +hasToken(f3, "hello world") +f5 BETWEEN -1 AND -1e-10 +f1 BETWEEN "\n" AND 1 +f2 >= f1 +f3 LIKE +(f2 <= TRUE) AND (f4 BETWEEN f2 AND -1e-10) +f5 === "abc" +hasAny(f1, 'O''Reilly') +f6 BETWEEN 1e10 AND f6 +(f4 IN ("\t", "\t", TRUE)) AND ((f5 IN (-1)) AND ((hasAll(f5, 999999999999)) AND (f4 IN (10, 3.14, "a_b%")))) +((f4 IN (-2.7, 10, f4, f3, "hello world")) AND (f5 IN (.5, ' ', .5, 'test', "hello world"))) OR (f1 NOT REGEXP 'O''Reilly') +f1 = TRUE +hasToken(f4) +f6 BETWEEN AND "\t" +f3 BETWEEN 'test' AND -.5 +hasToken(f1, 1e10) +f3 REGEXP "\n" +hasToken(f6, -2.7) +f5 BETWEEN 0.0000000001 AND "\t" +f5 IN ('') +f2 LIKE +f4 IS NULL +f5 IN () +f5 ILIKE "hello world" +hasAll(f3, -1e-10) +(f6 IN ('O''Reilly', 0, f2, 'O''Reilly')) OR (f1 IN (-1e-10, f4, 6.02e23, TRUE)) +f3 CONTAINS "a_b%" +has(f5, "\n") +f6 BETWEEN 'test' AND "xyz%" +f1 = "a_b%" +f1 IN (-.5, 'test', -2.7, '', -.5) +f2 REGEXP '' +f4 == f2 +(f3 NOT LIKE 'O''Reilly') OR (f2 IN (f5, ' ')) +f1 IN (,) +f3 = 'unclosed +(has(f1, 0)) OR (f5 > f4) +(f1 IN (' ', -2.7, "hello world")) OR (f5 IN (f5, 999999999999)) +f4 != "\t" +f5 = "xyz%" +has(f5, "\n") +f3 IN (,) +f3 REGEXP "a_b%" +f6 = "\n" +f3 IN (10) +hasAny(f4, 999999999999) +f4 BETWEEN "\n" AND "abc" +f4 BETWEEN 1e10 AND -3.5e-2 +f2 CONTAINS "\n" +f1 IS NULL +f3 BETWEEN 6.02e23 AND -3.5e-2 +(f5 IN (10, ' ')) AND (hasAny(f4, 'O''Reilly')) +f1 BETWEEN AND -1e-10 +hasToken(f5, -3.5e-2) +f6 > 3.14 +(f4 < 3.14) AND (has(f5, -1e-10)) +f3 LIKE "\t" +() +() +f5 IN ('', -3.5e-2, 6.02e23) +(f4 IN (1e10, "hello world", f2)) OR (f4 IN ("a_b%", -2.7, -3.5e-2)) +hasAll(f6, f1) +f4 BETWEEN f5 AND TRUE +f1 NOT ILIKE "xyz%" +f2 != -10 +(hasAll(f4, 999999999999)) OR (f1 BETWEEN 'O''Reilly' AND 'test') +f3 BETWEEN 'test' AND "%wild%" +f4 OR OR -.5 +f2 IN () +(f4 LIKE "%wild%") AND ((f1 NOT REGEXP ' ') OR (f1 LIKE '')) +f1 <> 1 +f6 != 999999999999 +f1 IN (-2.7, TRUE) +f2 BETWEEN -1e-10 AND 6.02e23 +f1 IN (-.5, -3.5e-2, TRUE) +f6 = NULL +(f3 IN ("\t")) OR (hasToken(f4, f6)) +f1 = 'unclosed +f6 = 'unclosed +((f1 > f2) AND (hasAny(f4, 1e10))) AND ((f3 < "%wild%") OR (f4 BETWEEN f4 AND f1)) +f6 BETWEEN "\n" AND 0 +f2 IN () +f1 IN (f1, f5, "\t", 999999999999) +f5 == 'test' +(f4 IN (1e10)) AND ((has(f6, -1)) AND (f1 IN ("%wild%", .5, f5))) +f3 = NULL +hasToken(f5, TRUE) +f5 IN () +(f3 NOT LIKE 'O''Reilly') OR (f2 CONTAINS "a_b%") +f6 IN (3.14, 1) +f4 NOT LIKE "\t" +f6 BETWEEN 10 AND 6.02e23 +has(f4, "%wild%") +hasAny(f4, "%wild%") +f3 === -3.5e-2 +f3 BETWEEN f1 AND -1 +(has(f5, -1)) OR (f6 == "a_b%") +f6 IN ('test') +f5 > 0 +((f6 REGEXP "hello world") OR (f4 BETWEEN "xyz%" AND -10)) AND (f4 BETWEEN 'O''Reilly' AND f2) +((((f3 > 999999999999) OR (f1 < -.5)) OR (f5 == f3)) OR ((f2 IN ("abc", "\t", "%wild%")) AND (f4 ILIKE "abc"))) AND (has(f6, -3.5e-2)) +f4 NOT LIKE 'O''Reilly' +hasAny(f5) +f6 > -10 +f6 IN ("abc", .5) +f6 BETWEEN ' ' AND 999999999999 +has(f4, "\n") +(f4 NOT REGEXP '') OR (f3 BETWEEN -1e-10 AND f3) +f1 BETWEEN .5 AND "a_b%" +f1 BETWEEN 0 AND -1e-10 +hasAll(f2) +f3 == TRUE +hasAll(f3, -3.5e-2) +(f1 BETWEEN '' AND 'O''Reilly') AND (f5 = 999999999999) +((f2 == ' ') OR ((f6 BETWEEN "\n" AND f1) OR (f2 <= 'O''Reilly'))) OR ((f1 IN ("a_b%", -3.5e-2, 1e10)) AND (f2 BETWEEN "a_b%" AND 999999999999)) +f5 IS NULL +f6 BETWEEN 0.0000000001 AND ' ' +(f1 CONTAINS 'O''Reilly') AND (f3 NOT LIKE "hello world") +((f3 IN (6.02e23)) OR (f4 IN (999999999999, 0, f2, "%wild%", 0))) AND (f2 ILIKE ' ') +f6 BETWEEN "hello world" AND .5 +f6 > -.5 +hasToken(f6, -1e-10) +f4 BETWEEN -10 +f2 = NULL +f4 BETWEEN f6 AND TRUE +f1 <> 999999999999 +f5 ILIKE "xyz%" +f1 LIKE +f3 = 'unclosed +hasAny(f3, "\t") +f5 ILIKE "hello world" +f4 IN () +f5 IN (-1e-10, -3.5e-2) +f5 LIKE '' +f4 = -1e-10 +(f6 IN (-10, 3.14, 1e10, 10)) AND (f2 LIKE "\t") +f5 BETWEEN '' AND "%wild%" +f6 != 6.02e23 +() +f5 BETWEEN ' ' AND -10 +f2 >= f4 +(f2 = -3.5e-2 +hasAll(f6, 1e10) +f4 NOT REGEXP "xyz%" +(hasAll(f4, f3)) OR (f1 BETWEEN "\t" AND 6.02e23) +f4 === 'O''Reilly' +f5 IN [1 2 3] +f1 IN (.5, -10, f2, TRUE, "abc") +hasAll(f1) +f2 = .5) +f5 NOT ILIKE "\t" +NOT +f1 NOT REGEXP ' ' +f1 IN ("abc") +f2 BETWEEN TRUE AND "%wild%" +f6 > -.5 +hasAll(f2, 3.14) +f2 = f1) +f3 LIKE +f5 IN (,) +f2 BETWEEN '' AND "abc" +AND f6 = f6 +f4 IN (3.14, "\t") +f4 LIKE "hello world" +f5 ILIKE ' ' +(hasToken(f3, f6)) OR (f5 == -1e-10) +f5 NOT +f6 = 1e10 +f2 IN () +AND f3 = 1 +NOT +hasAny(f2, "hello world") +f3 == '' +f2 = 999999999999 +f4 NOT LIKE ' ' +(f6 CONTAINS '') OR (f3 ILIKE "\t") +AND f5 = -.5 +f3 IN (10, "a_b%", '') +f4 IN (-3.5e-2, 3.14, 0.0000000001, TRUE) +f6 LIKE +(f1 BETWEEN 1 AND -10) OR (f6 <= f1) +NOT +f6 = -3.5e-2) +f4 IN (-2.7, f1, "a_b%") +f4 NOT LIKE "a_b%" +f1 == -.5 +f1 NOT LIKE '' +f6 > -1e-10 +f5 OR OR '' +f3 = "abc" +hasAll(f6, 10) +AND f4 = f6 +hasToken(f6, '') +f6 = -10 +f5 BETWEEN 999999999999 AND 0 +f1 IN ("abc") +f3 REGEXP "\t" +f1 < -1 +f4 BETWEEN f2 +f6 BETWEEN f3 AND 0 +f4 BETWEEN -10 AND .5 +f5 BETWEEN TRUE AND "hello world" +hasToken(f2, -.5) +f1 CONTAINS 'test' +f6 NOT CONTAINS "xyz%" +(f2 IN (TRUE, 'O''Reilly', 1e10, "\n", -2.7)) OR (f6 IN (f3, 3.14, "xyz%", -2.7, -.5)) +f4 BETWEEN f4 AND -1 +hasAll(f4, "%wild%") +f2 REGEXP 'O''Reilly' +f5 IN [1 2 3] +f1 NOT CONTAINS 'O''Reilly' +() +f6 = NULL +(f3 BETWEEN f2 AND 1e10) AND (f6 ILIKE ' ') +(f2 = 'test' +f2 IN (-10, -1) +f5 BETWEEN AND "a_b%" +f4 IS NULL +has(f3, 3.14) +f2 BETWEEN AND ' ' +f6 = +f3 OR OR "\n" +f4 BETWEEN '' AND f1 +f1 BETWEEN -3.5e-2 AND .5 +hasAny(f5, 999999999999) +f3 <> f3 +((((f1 >= 1e10) AND (f6 <> 6.02e23)) OR (hasAny(f5, 6.02e23))) AND ((f3 CONTAINS ' ') OR (hasToken(f3, "\n")))) OR (f2 BETWEEN "hello world" AND .5) +f3 OR OR -1 +(f3 NOT REGEXP "\t") OR (hasToken(f3, 'O''Reilly')) +(f3 BETWEEN -.5 AND f2) AND (f4 IN (-.5, '', 3.14, "\t", ' ')) +(f2 NOT REGEXP "xyz%") AND (hasToken(f4, "\t")) +(f3 IN (f4, f5, f2, 'O''Reilly', f2)) OR (f5 BETWEEN f2 AND -10) +f1 = 10) +hasToken(f5, f2) +f6 IN (0, "%wild%") +f6 > f4 +f4 IN (6.02e23, -10) +f4 IN (3.14, "\t", "a_b%") +NOT +f5 LIKE '' +((f3 NOT LIKE "\t") OR (f4 LIKE "xyz%")) OR (f6 > "abc") +f1 NOT LIKE "a_b%" +f2 BETWEEN "\n" AND 'O''Reilly' +f3 BETWEEN "\n" AND '' +((f6 IN (f2, -1e-10, "a_b%", .5)) OR (hasAny(f2, 0))) AND (f4 BETWEEN .5 AND 3.14) +f5 = 10 +(has(f5, -1)) OR (f6 NOT REGEXP "a_b%") +(f3 BETWEEN 999999999999 AND 6.02e23) AND (f5 BETWEEN -10 AND -1e-10) +(f5 <> TRUE) OR (hasAll(f6, "abc")) +has(f6, "hello world") +hasAny(f4, -.5) +f3 REGEXP "a_b%" +f1 NOT ILIKE ' ' +f3 BETWEEN 'O''Reilly' AND "\t" +f5 IS NULL +f4 != f1 +(hasToken(f5, 1e10)) OR (f3 IN ("abc", 3.14, 6.02e23, "hello world")) +AND f6 = 1e10 +f1 BETWEEN "%wild%" AND -2.7 +f6 BETWEEN "\n" +f6 <= "%wild%" +f4 BETWEEN 0 AND 0 +f4 === '' +f4 === "abc" +f5 IN (f1, "hello world", 0.0000000001) +f2 != 999999999999 +f5 OR OR f4 +has(f2, f1) +f3 IN () +f5 IN (-1e-10, -3.5e-2, 999999999999, 1, f4) +f1 IN ("%wild%", f3) +f4 LIKE ' ' +hasAny(f6, 'O''Reilly') +(f6 IN ("a_b%", f2, -10, -2.7)) AND (f3 <= f5) +(f3 BETWEEN -3.5e-2 AND "\n") OR (f5 IN (0.0000000001, "hello world", f1, 'test')) +f6 BETWEEN -2.7 AND 0 +f1 = NULL +f4 BETWEEN -10 AND 6.02e23 +f1 BETWEEN 0 AND 1e10 +f2 LIKE ' ' +f6 REGEXP "\n" +f1 OR OR "hello world" +f5 BETWEEN "%wild%" AND 1 +f1 >= 0 +f1 != 1e10 +f3 <> -.5 +f5 <> ' ' +f5 = 'test' +f4 == 0 +f3 = 'unclosed +f4 NOT REGEXP "abc" +hasAll(f5, f3) +f2 >= -1e-10 +f2 > 10 +f3 IN ("\t") +f2 NOT +f6 NOT +f3 != f6 +f1 = .5 +f3 NOT ILIKE ' ' +f6 NOT LIKE "abc" +f2 = NULL +f6 BETWEEN 6.02e23 AND 999999999999 +f3 IN (.5) +f4 >= "\t" +hasToken(f4, 3.14) +(f1 > 3.14) AND (hasAny(f4, "hello world")) +f5 BETWEEN 999999999999 AND 999999999999 +f5 BETWEEN "\t" +f3 BETWEEN '' +(f5 <> -.5) AND (f1 REGEXP 'O''Reilly') +hasAll(f6, 1) +f6 BETWEEN 3.14 AND "abc" +(f1 IN ('O''Reilly', 1e10, 0.0000000001, -3.5e-2, "\t")) OR (f4 IN (-10)) +f2 NOT ILIKE ' ' +f1 <= '' +f3 < f5 +f5 NOT ILIKE "xyz%" +hasToken(f2, -3.5e-2) +f4 NOT REGEXP "%wild%" +f6 = +f4 IN (-2.7, 3.14, 1e10, -.5) +f6 = +f2 IN ("\n", -1e-10) +f5 IN (f6, TRUE) +f1 BETWEEN "\t" +hasAll(f4, -3.5e-2) +f5 ILIKE "a_b%" +(f5 = f3 +f3 BETWEEN 'test' AND -2.7 +f5 > 0.0000000001 +hasAny(f5, 3.14) +f2 = NULL +(f2 BETWEEN f6 AND 3.14) AND (f6 NOT CONTAINS "xyz%") +f4 BETWEEN AND f2 +f4 LIKE +hasAll(f1, 0) +f1 = 3.14 +f6 != .5 +has(f5, "xyz%") +f1 = NULL +NOT +(f5 NOT LIKE "a_b%") AND (f4 BETWEEN "%wild%" AND f2) +f6 == 'test' +(f5 != 0) AND (f5 BETWEEN -.5 AND "\n") +f5 >= "\n" +f2 <= "\t" +has(f5, "\n") +hasAny(f5, f3) +f6 OR OR f3 +f4 = +f3 LIKE "%wild%" +f6 BETWEEN "abc" AND f4 +f5 BETWEEN "abc" AND "hello world" +f6 IN (1e10) +hasToken(f5, "%wild%") +f2 NOT REGEXP 'O''Reilly' +f1 = 'unclosed +f5 IN (-3.5e-2, f6, 0, -3.5e-2) +NOT +f6 LIKE ' ' +has(f6, 0) +f6 BETWEEN 3.14 AND ' ' +f3 BETWEEN 'test' AND 3.14 +f3 NOT CONTAINS "abc" +f5 IN (f5, .5, .5, f2) +f2 BETWEEN AND 'test' +NOT +f5 BETWEEN f5 AND 10 +(f1 BETWEEN '' AND "\t") OR (f1 NOT LIKE "\t") +f1 IN [1 2 3] +f2 LIKE +f5 IN () +f1 IN ("xyz%", 3.14, "hello world", TRUE) +f3 OR OR "abc" +f3 NOT LIKE "a_b%" +f2 LIKE 'test' +f5 BETWEEN TRUE AND 'O''Reilly' +f1 IN (f2) +f3 OR OR -10 +f4 IN ("\t") +hasToken(f4, f5) +f2 < -10 +f2 IN ("hello world", f1, "hello world") +f1 IN () +f3 = 'unclosed +f1 LIKE "abc" +(f2 <= "hello world") AND (hasToken(f3, 1)) +hasAny(f1, 1) +hasToken(f1, 999999999999) +(f6 BETWEEN "abc" AND "\t") AND (f5 IN (-.5, f1)) +(f6 != 'test') AND (f5 BETWEEN 999999999999 AND 0) +f4 = .5) +f4 >= "\t" +(f1 NOT CONTAINS "\n") AND (f4 != 6.02e23) +(hasAny(f1, 999999999999)) OR (f1 BETWEEN '' AND "abc") +f6 BETWEEN f3 AND -.5 +f6 IN ("hello world") +(has(f3, "hello world")) OR ((f4 != 1e10) AND (hasAny(f6, 'test'))) +has(f2, "\t") +f5 BETWEEN f5 AND 0 +f6 REGEXP "\t" +f6 === 6.02e23 +f2 IN (,) +f5 BETWEEN f5 AND "hello world" +hasAll(f1, "xyz%") +(f4 REGEXP 'test') OR (f1 IN (' ')) +f6 IN ("\n") +f3 BETWEEN 999999999999 AND 1e10 +(f3 IN (0.0000000001, "hello world", '')) AND (hasAll(f5, "a_b%")) +f5 IS NULL +f4 ILIKE "a_b%" +f1 IS NULL +f3 OR OR -.5 +hasAny(f6, 0) +f5 ILIKE "abc" +NOT +f2 = 'unclosed +(f5 NOT LIKE 'test') AND ((f5 ILIKE "\n") AND (f3 BETWEEN 3.14 AND 1)) +(f4 BETWEEN -.5 AND '') OR (hasAny(f5, "%wild%")) +f6 IN (,) +f6 >= f4 +f2 >= -2.7 +f1 IN ("a_b%", 6.02e23, 'test', "\n") +hasAll(f5, -3.5e-2) +(f1 = f6 +f1 IN [1 2 3] +f2 = +f2 IN [1 2 3] +f6 BETWEEN "%wild%" +f1 BETWEEN AND 'O''Reilly' +f2 BETWEEN "\t" AND 0.0000000001 +f5 IN () +hasToken(f6, TRUE) +f6 < 0.0000000001 +f4 == -10 +f1 BETWEEN .5 AND f6 +f5 IN (.5, 6.02e23, "\n") +has(f3, f4) +f6 CONTAINS "%wild%" +f3 <> 3.14 +f3 LIKE "abc" +has(f5, .5) +(f1 = "abc" +f5 IN (3.14, f6, f6) +hasAll(f4, "a_b%") +f5 NOT LIKE "a_b%" +f4 BETWEEN -1 AND 999999999999 +hasAll(f6, 999999999999) +f5 IN (10, -1, -10) +f4 > "\t" +f5 <> -1e-10 +hasAny(f2) +f3 LIKE "a_b%" +has(f5, f1) +has(f1, "\n") +f5 == f2 +f1 IN ("a_b%") +f5 <= "%wild%" +f3 REGEXP "a_b%" +(f5 BETWEEN -1e-10 AND 3.14) OR ((f6 NOT REGEXP 'O''Reilly') AND (f4 IN (-2.7, 6.02e23, TRUE, f6, .5))) +((f6 NOT ILIKE "hello world") AND (f5 >= -1)) AND (f4 == f3) +f2 BETWEEN 1e10 AND "a_b%" +has(f5, 3.14) +((f6 BETWEEN f2 AND f3) OR (f3 == 1)) OR (hasAll(f3, -.5)) +f4 LIKE "abc" +(f5 IN (TRUE, "a_b%", f5, 3.14)) AND (f3 IN (' ', 'O''Reilly', f2)) +f1 IN ("\t", -10, "xyz%", 1, -10) +f6 === 1 +f2 IN (TRUE) +hasToken(f5, 999999999999) +f6 NOT +f1 BETWEEN "abc" AND -2.7 +f3 NOT +f4 >= 0.0000000001 +f1 IN (TRUE, f6, 10, -2.7, -.5) +f4 = 1e10) +f6 NOT CONTAINS "hello world" +f3 BETWEEN -.5 +f2 IN (0, 3.14, -.5, 999999999999) +f6 BETWEEN 10 AND 10 +(f3 <= f4) OR (f4 == '') +f1 > "\t" +hasAll(f2, TRUE) +(f3 NOT ILIKE "\t") AND (hasAny(f3, "abc")) +has(f2, ' ') +(f3 NOT REGEXP '') OR (f4 <= -1e-10) +f6 BETWEEN 10 AND 999999999999 +AND f2 = f1 +f1 === f5 +hasAny(f2, 'test') +(f5 NOT LIKE "\n") OR (f6 IN ('', f4, f1)) +hasToken(f6, 6.02e23) +f4 NOT LIKE "xyz%" +(f2 < '') OR ((f6 >= "%wild%") OR (f5 IN (999999999999, 0))) +f2 BETWEEN 'test' AND 1e10 +f1 CONTAINS "%wild%" +f6 BETWEEN -1e-10 AND ' ' +f5 IS NULL +f1 LIKE +f4 <> -10 +f5 == 1 +(f3 = 'O''Reilly' +f5 === 3.14 +(f5 BETWEEN 1e10 AND 0) AND (f2 IN ('O''Reilly', "hello world")) +hasToken(f1, TRUE) +(f3 NOT CONTAINS ' ') OR (f3 IN (' ', -3.5e-2)) +f3 <= "abc" +(f2 = -10 +hasToken(f3, .5) +f2 BETWEEN 999999999999 AND "hello world" +(f3 IN ('O''Reilly', 'O''Reilly', ' ', "\t")) OR (f3 IN ("xyz%")) +(f2 NOT LIKE 'O''Reilly') AND (f6 LIKE ' ') +f5 BETWEEN 3.14 AND "hello world" +hasToken(f2, "\t") +AND f6 = .5 +f6 = 'unclosed +hasToken(f6, 1) +hasAny(f5, "xyz%") +f2 LIKE ' ' +f5 BETWEEN .5 AND 3.14 +NOT +(f1 IN (-1, "\t", f6)) AND (f1 IN ("xyz%")) +f5 NOT REGEXP "%wild%" +f6 IN (,) +hasAll(f1, "hello world") +f4 IN (,) +(f5 BETWEEN "xyz%" AND "a_b%") OR (f3 IN (3.14)) +f2 BETWEEN -10 AND f6 +f2 == -.5 +f3 === 0.0000000001 +f1 IN (f1, TRUE, f2, -1) +f6 BETWEEN AND "\t" +f6 = f4 +f6 IN (TRUE, 1e10, '') +has(f3, .5) +f3 BETWEEN "%wild%" AND -1e-10 +f3 BETWEEN 0.0000000001 AND "xyz%" +f4 BETWEEN "xyz%" AND "\n" +f4 BETWEEN 10 AND f1 +f4 IN ('test', 1, 999999999999) +f5 IN (1e10, .5) +(f6 IN (1e10, 1, ' ', '')) OR (f2 BETWEEN -2.7 AND "\t") +f6 = +hasToken(f2, 10) +f5 BETWEEN f6 AND 'test' +(f5 = -2.7 +f6 IN (' ', 'test') +f6 LIKE +f6 BETWEEN f2 AND 1 +f4 BETWEEN "xyz%" AND -.5 +f1 = NULL +has(f1, "xyz%") +hasAll(f1, 1) +f2 IN (f2, 'test', -1) +f4 IN ("xyz%") +hasToken(f5, f5) +f3 IN [1 2 3] +f4 < 0 +f4 NOT ILIKE 'test' +hasAll(f6, f4) +f5 IS NULL +f4 NOT +f6 IN (,) +f6 == -2.7 +NOT +f5 ILIKE "a_b%" +f3 BETWEEN 999999999999 AND 1e10 +f1 == 1 +hasAny(f5, .5) +f5 IN () +f4 IN (3.14, "a_b%", f4) +f5 < -.5 +hasAny(f1, -10) +f6 IN (6.02e23, "xyz%", f4, 0, f6) +f6 = -1e-10) +f3 BETWEEN "hello world" AND 1e10 +(f2 <= -2.7) OR ((f4 CONTAINS "\n") AND (f4 != f3)) +f6 BETWEEN .5 AND f2 +f5 OR OR ' ' +() +f2 >= -1e-10 +f6 BETWEEN "\n" AND "hello world" +f1 IN (f3, 10, "xyz%") +f1 IN ("abc", f5, -3.5e-2) +(f1 ILIKE 'test') AND (f6 = 0) +f5 BETWEEN f4 AND 0 +f6 ILIKE "\t" +f6 IN (-1e-10, 0, 6.02e23) +f1 BETWEEN -.5 AND f6 +f5 LIKE 'test' +f3 IN (,) +f1 IN (10, 999999999999, 3.14, "xyz%") +f5 IN (,) +f1 NOT CONTAINS "xyz%" +(hasAny(f4, 6.02e23)) AND (f6 BETWEEN 0.0000000001 AND f2) +f6 IS NULL +f1 LIKE 'O''Reilly' +f5 BETWEEN AND f5 +f5 IN (10, 999999999999, f2) +f2 = NULL +f6 CONTAINS ' ' +has(f5) +f5 BETWEEN 999999999999 AND f5 +f5 IS NULL +f5 != f3 +f5 IN [1 2 3] +f3 IS NULL +f2 LIKE "xyz%" +(f1 == f2) OR ((f6 IN (f3, f3, 'test', f4)) AND (f1 IN (TRUE))) +f3 IN (f6, -1, f5) +f6 IN [1 2 3] +f2 BETWEEN 'O''Reilly' +(f5 BETWEEN -1e-10 AND 10) AND (hasAny(f1, '')) +f2 >= 0.0000000001 +(f6 IN ("hello world")) AND (hasToken(f5, '')) +hasAny(f4, -.5) +(f4 BETWEEN -1 AND 6.02e23) AND ((hasToken(f5, 999999999999)) AND ((f2 LIKE 'O''Reilly') OR (f5 CONTAINS 'test'))) +(f2 = -1 +hasToken(f6, "xyz%") +f5 IN (.5) +f1 == "%wild%" +f4 BETWEEN 1e10 AND "a_b%" +f2 LIKE "xyz%" +f6 = +(f2 NOT CONTAINS "a_b%") AND (f6 > -3.5e-2) +(f2 BETWEEN -10 AND .5) OR (f3 BETWEEN -10 AND "\n") +f5 BETWEEN -3.5e-2 AND TRUE +f5 IN (-.5, f3, f2, f3) +hasAll(f5, TRUE) +f6 BETWEEN 'O''Reilly' AND "\t" +f3 = NULL +(f2 ILIKE "\n") AND (f4 NOT ILIKE "%wild%") +f3 = NULL +f5 != TRUE +(f5 >= 10) AND (f2 BETWEEN "a_b%" AND 10) +f5 <> 0.0000000001 +f2 BETWEEN f4 AND "xyz%" +f3 IN ('test', 'test', f4) +f4 IN (,) +f5 CONTAINS ' ' +hasAll(f6, -.5) +(f6 = -1e-10 +hasAll(f2, 999999999999) +f5 IN () +f4 === .5 +AND f4 = -3.5e-2 +f5 = +f2 > -.5 +f2 IN (-1, -10) +has(f6, 6.02e23) +hasToken(f4, f1) +f6 NOT +hasToken(f2, ' ') +f4 REGEXP '' +(f6 <= f5) AND (hasAll(f3, -10)) +f5 < f1 +has(f3, "xyz%") +f5 REGEXP "\t" +hasAll(f2, 10) +f1 BETWEEN f2 +((f2 != "%wild%") AND (((f2 >= -3.5e-2) OR (f3 < '')) OR ((f2 = f2) OR (f5 != "%wild%")))) OR (f4 NOT CONTAINS "hello world") +f4 NOT CONTAINS '' +f4 > 3.14 +hasToken(f1, f1) +f5 REGEXP "abc" +hasToken(f1, 999999999999) +f2 BETWEEN .5 AND f5 +hasAny(f1, "\t") +(f3 IN (1, 1e10, 3.14)) OR (f3 LIKE "\t") +(f6 = 0.0000000001 +f2 BETWEEN 0.0000000001 AND ' ' +f1 OR OR TRUE +f1 NOT CONTAINS "abc" +f2 IN (-2.7, f5, '') +f4 != f1 +f1 IN [1 2 3] +f1 BETWEEN f5 +f5 NOT CONTAINS '' +f1 IN [1 2 3] +hasToken(f5, 'test') +(f2 BETWEEN 3.14 AND f4) OR (hasAll(f3, "a_b%")) +f6 NOT CONTAINS "abc" +hasAll(f6, "%wild%") +hasAny(f6, 'O''Reilly') +f4 IN (0.0000000001, -3.5e-2, f3) +f1 IN ("a_b%") +f4 BETWEEN f3 AND 10 +AND f6 = "\n" +has(f2, -1) +f4 IN (-2.7, f1) +f6 IN (-1, "\t", "\t", 'O''Reilly') +f2 BETWEEN "abc" AND f5 +(f5 BETWEEN -1e-10 AND 0.0000000001) AND ((f5 IN (6.02e23, ' ')) OR (hasAny(f3, f6))) +f6 = NULL +(f5 = 1) AND (has(f1, -1)) +f6 BETWEEN TRUE AND "\t" +f4 = f6 +f3 REGEXP "\n" +AND f5 = "\t" +f5 OR OR 6.02e23 +(f3 = f3 +f4 = NULL +f1 IN (,) +AND f4 = .5 +f2 = NULL +((f3 NOT LIKE 'O''Reilly') AND (f3 CONTAINS "%wild%")) AND (f5 BETWEEN f3 AND -1e-10) +f2 OR OR TRUE +f6 BETWEEN 3.14 +f5 IN ("abc", 0, 3.14) +f4 IN [1 2 3] +f3 IN (-10, 0) +(hasToken(f5, 0)) OR (f2 <= 1) +f2 = NULL +f1 = 'test' +f3 IN (' ', "%wild%") +(f2 ILIKE "hello world") AND ((f3 IN (-2.7, f3, 3.14)) AND (f4 IN (999999999999, 6.02e23, 999999999999, TRUE))) +NOT +f6 IS NULL +f3 <> 0 +f4 ILIKE ' ' +AND f2 = "%wild%" +f2 ILIKE "a_b%" +f3 BETWEEN 999999999999 AND f3 +((f1 BETWEEN -10 AND "xyz%") OR (f4 IN ('', 1, "abc", 10))) OR ((f5 > -3.5e-2) AND (f5 BETWEEN f5 AND -2.7)) +f1 IN [1 2 3] +hasAny(f5, "xyz%") +has(f4, 10) +f4 REGEXP "\t" +f3 IN ('O''Reilly', 6.02e23, -3.5e-2, ' ', 1e10) +AND f4 = ' ' +f4 NOT REGEXP "abc" +f2 BETWEEN '' AND -.5 +f3 REGEXP "hello world" +f2 LIKE +f5 = NULL +f1 BETWEEN 1e10 AND "xyz%" +hasToken(f2, 999999999999) +f1 BETWEEN f5 AND "%wild%" +f2 BETWEEN f3 AND -1e-10 +f4 CONTAINS 'test' +(f5 IN (f6, 6.02e23, f3)) OR (f3 IN (-10, ' ', f5, 0.0000000001)) +((has(f3, "hello world")) OR ((hasAny(f5, TRUE)) OR (f1 LIKE ' '))) OR (f3 <= 3.14) +(f3 IN (-1)) OR (f5 IN (.5, "\t", "\n", -2.7, 3.14)) +has(f5, -10) +(f2 NOT CONTAINS "abc") AND ((f5 NOT ILIKE "\t") AND (f4 IN ("xyz%", 1e10))) +f3 == -2.7 +f4 LIKE "a_b%" +f2 IN (-.5, -10, 'O''Reilly', f2) +(((f4 BETWEEN -2.7 AND -1) OR (f2 BETWEEN "%wild%" AND f2)) OR (has(f2, 1e10))) AND (f2 < f6) +(f4 IN (999999999999, 3.14, f2)) AND (f6 = "hello world") +() +() +f2 > 'test' +f1 NOT +f4 REGEXP ' ' +hasToken(f6, -.5) +f2 IN (.5, -1e-10, 3.14, 'test') +NOT +has(f3, 1e10) +f6 NOT CONTAINS "%wild%" +f1 BETWEEN 10 AND f1 +f4 OR OR 'test' +() +f2 REGEXP 'O''Reilly' +(f1 < -3.5e-2) OR (f6 NOT REGEXP 'O''Reilly') +f4 BETWEEN "abc" AND "%wild%" +f5 IN (-.5, 'O''Reilly', TRUE, f6) +f6 === -3.5e-2 +f3 CONTAINS "\t" +f2 BETWEEN "xyz%" AND f5 +f2 === "%wild%" +AND f6 = -1 +f1 BETWEEN AND -1e-10 +f2 IN [1 2 3] +f4 BETWEEN "%wild%" AND f6 +has(f3, f4) +f6 IN (0.0000000001) +f3 IN ("a_b%", "abc", -3.5e-2) +f4 NOT ILIKE ' ' +f5 = "\t") +f3 BETWEEN 1e10 AND f6 +f5 IN (,) +f4 LIKE "a_b%" +f6 IN (.5, -10, "xyz%", -1, f1) +f4 IN (' ', -2.7, 1e10, f1) +((hasToken(f1, ' ')) AND (f1 IN (f6, "abc"))) OR (f4 IN (999999999999, 0.0000000001)) +hasToken(f3, "%wild%") +(f2 BETWEEN 999999999999 AND 10) OR (f4 LIKE "a_b%") +f4 NOT REGEXP "\t" +(f1 = -2.7 +hasAny(f4, f6) +f6 IN (-2.7, 'test') +f4 BETWEEN 999999999999 AND "%wild%" +(hasAny(f1, 3.14)) AND (((hasToken(f4, "xyz%")) OR (f5 CONTAINS '')) OR (f2 BETWEEN 0 AND f3)) +f1 <= "xyz%" +f6 != "\t" +f4 NOT +f1 = NULL +f1 REGEXP "hello world" +f3 LIKE 'O''Reilly' +f2 BETWEEN "%wild%" AND f6 +f6 BETWEEN f3 AND 1 +f6 <> ' ' +f2 IN (.5) +f6 IN (1e10, "xyz%", "\t", -1, .5) +f2 BETWEEN AND ' ' +f5 <= f1 +f3 != ' ' +NOT +f5 IN ("hello world", f6, f5, "\t", 3.14) +f3 REGEXP "xyz%" +f1 BETWEEN AND 6.02e23 +f4 BETWEEN 6.02e23 AND 0.0000000001 +f3 NOT ILIKE "%wild%" +f5 BETWEEN f4 AND 10 +f1 CONTAINS "hello world" +f5 IN [1 2 3] +f1 REGEXP "%wild%" +f5 IN (f6, ' ') +f1 <= f1 +hasToken(f4, f2) +f2 IN ('O''Reilly', f2) +f2 = +hasToken(f6, .5) +((f6 BETWEEN -1e-10 AND "\t") AND (f6 LIKE "\t")) AND (f1 NOT CONTAINS "xyz%") +f5 IN (1, f3, 'test', "\t", .5) +f3 <= "%wild%" +f3 CONTAINS "%wild%" +f6 NOT ILIKE "%wild%" +((f5 BETWEEN 6.02e23 AND -.5) AND (f1 BETWEEN 'test' AND 1e10)) AND (f2 >= 1e10) +f3 BETWEEN 3.14 +f6 IN [1 2 3] +f5 IN (,) +f3 LIKE "\t" +f1 IN ('test', 3.14, 3.14, 0.0000000001) +hasAny(f2, TRUE) +(hasAny(f2, f1)) AND (f2 = 1) +f2 BETWEEN -1e-10 AND "abc" +f6 = f5) +f3 IN (3.14, "a_b%", -2.7, f6) +f1 <= 'test' +f5 BETWEEN 1e10 AND -3.5e-2 +() +has(f6, 0) +f2 BETWEEN 6.02e23 AND f5 +(has(f2, 3.14)) OR (f1 IN (f6)) +AND f2 = "hello world" +f1 IN (f6, "%wild%", "xyz%", f4) +f2 <> -.5 +(f1 == '') OR (f5 == 0) +f6 BETWEEN f2 AND 1e10 +f2 BETWEEN 'O''Reilly' +f2 NOT LIKE "abc" +f2 IN () +f6 NOT CONTAINS "hello world" +f2 LIKE 'O''Reilly' +hasAll(f4, 3.14) +f6 < -10 +f5 != ' ' +f6 = f4) +f1 IN (6.02e23, f6, f6) +f3 <= 1 +f3 = NULL +f3 != 10 +f4 === f2 +(f3 IN (0.0000000001)) AND ((f5 IN (1e10)) AND (f2 >= f4)) +f2 BETWEEN 999999999999 AND "a_b%" +f3 BETWEEN 1 AND f1 +f6 IN ('') +f5 <> -3.5e-2 +f1 REGEXP "xyz%" +f2 IN ("hello world", 0.0000000001, 'O''Reilly') +f6 != 'O''Reilly' +f6 BETWEEN AND "a_b%" +f2 = "%wild%" +f5 == '' +f2 IN (0, f1, -.5) +f5 IN ("hello world") +hasAll(f6, "abc") +f2 BETWEEN 999999999999 AND -10 +hasToken(f2, ' ') +has(f3, -3.5e-2) +f1 IN () +f6 BETWEEN "hello world" AND "\t" +hasAll(f2, -2.7) +f6 != -1 +f1 IN (f6, 0.0000000001, "hello world", 1) +f1 CONTAINS "\t" +(hasAny(f6, .5)) OR (f3 BETWEEN f1 AND -1e-10) +f3 IN ("abc", f3) +f3 === f5 +f6 = -1 +hasToken(f2, "%wild%") +f1 = +() +f1 IN (,) +((f4 IN (1e10, f5, TRUE, -1e-10)) AND (f3 > 3.14)) OR ((f1 REGEXP "a_b%") AND ((f1 BETWEEN 'test' AND 0.0000000001) OR (has(f3, '')))) +f5 = NULL +() +(f5 BETWEEN 3.14 AND -10) AND ((f4 > '') OR (hasAny(f4, -10))) +f2 IN (,) +NOT +hasToken(f1, 3.14) +f6 NOT LIKE "\n" +(f6 = .5 +f3 IN ('O''Reilly', f1) +AND f4 = f2 +f4 === "xyz%" +f5 IN (1e10, "xyz%", '') +f2 <> "abc" +hasAll(f1, "hello world") +(f6 IN ("xyz%")) AND ((hasAny(f6, f4)) AND (f6 REGEXP "\n")) +hasAll(f3, f5) +f5 IN (,) +f1 BETWEEN AND 0.0000000001 +f3 IN ("\t", -1e-10, 'test') +f3 == ' ' +f6 NOT LIKE '' +f3 ILIKE "hello world" +f6 LIKE +hasToken(f2, "hello world") +f3 NOT REGEXP "%wild%" +hasAny(f6, '') +f2 NOT ILIKE ' ' +f2 LIKE +f1 BETWEEN f5 AND 10 +f5 > 999999999999 +(f3 = 10 +f4 IN (f4, "xyz%", 'test') +f4 NOT CONTAINS 'test' +f1 BETWEEN "\t" AND 1 +f1 IN (f6, f3) +hasToken(f1) +f4 IN (-2.7, "a_b%") +(f4 BETWEEN f3 AND 3.14) AND (f6 NOT CONTAINS "abc") +f1 NOT LIKE "%wild%" +f2 BETWEEN 6.02e23 AND .5 +((f5 IN (-10, 'O''Reilly', "xyz%")) OR (f6 ILIKE "abc")) OR (f2 IN (f2, "\n")) +f5 NOT LIKE "%wild%" +f2 REGEXP 'test' +(f2 LIKE "abc") OR (hasAny(f5, 999999999999)) +(has(f5, f4)) OR (f6 BETWEEN '' AND TRUE) +f1 BETWEEN -2.7 AND f6 +f5 LIKE +f3 IN (,) +(f6 ILIKE "\n") AND (((f4 ILIKE "\n") AND (hasAny(f4, "\t"))) OR (f6 BETWEEN f1 AND f5)) +f2 CONTAINS "%wild%" +f2 IN ("abc", "\t", 0, 1) +NOT +has(f5, -2.7) +f2 IN (-.5, 0, f4, 0, f5) +(((hasToken(f6, .5)) AND (f4 IN (TRUE))) OR (f1 IN (0, -.5))) AND (f2 BETWEEN 1e10 AND 'O''Reilly') +f4 NOT LIKE "%wild%" +f6 NOT REGEXP '' +f2 CONTAINS "a_b%" +f1 IN [1 2 3] +f4 BETWEEN -10 AND f3 +f5 IN ('', f2, f2, 'test', -3.5e-2) +f5 IN ("xyz%", 10, f3, 1, "a_b%") +f1 BETWEEN -.5 AND f1 +f6 NOT REGEXP "hello world" +f4 REGEXP ' ' +hasToken(f1, TRUE) +f2 NOT LIKE "abc" +f4 >= f4 +(f2 != 0) AND (((f3 ILIKE 'test') AND (f2 BETWEEN "%wild%" AND "a_b%")) AND (f1 BETWEEN 'O''Reilly' AND -2.7)) +f3 IN (1e10, -2.7, 6.02e23, 1) +f6 IN ("xyz%") +f5 BETWEEN "\t" AND "%wild%" +f3 BETWEEN "a_b%" AND -2.7 +f3 CONTAINS "\n" +f3 NOT REGEXP "abc" +f6 = 'unclosed +f3 <> 0 +f2 IN (f6, f6) +f5 = NULL +f3 IN (10, f5, -2.7, -2.7, 0.0000000001) +f4 IN (10, "\n", f3, f6, "a_b%") +f2 == 6.02e23 +f4 LIKE "a_b%" +((f1 IN ("%wild%", 999999999999, "xyz%", 1e10, f2)) AND (f5 BETWEEN f3 AND f5)) AND (f5 BETWEEN 0 AND 1) +f3 <> -3.5e-2 +(((f6 IN (.5, 0, "xyz%")) AND (f2 BETWEEN 0 AND -1)) OR (f5 IN (1e10, -1e-10, 0.0000000001, "xyz%"))) OR (hasAny(f1, 'O''Reilly')) +hasAll(f1) +f1 IN (-2.7, "%wild%", TRUE) +f1 BETWEEN 1e10 AND "hello world" +f6 >= 6.02e23 +f6 CONTAINS "xyz%" +f2 > 999999999999 +f3 <= "hello world" +f2 NOT CONTAINS 'test' +f5 NOT CONTAINS "a_b%" +f2 BETWEEN "\n" AND -1e-10 +f6 == "a_b%" +hasAny(f5) +f1 REGEXP "\t" +(f6 BETWEEN f6 AND TRUE) AND (f2 REGEXP 'test') +f5 BETWEEN f3 AND -1e-10 +() +hasAny(f6, .5) +f6 != f4 +f6 BETWEEN 'test' AND f2 +f3 LIKE +(f2 IN ("\t")) AND (f4 IN (f4, "%wild%", f2, -2.7)) +f1 != 6.02e23 +(f1 < -3.5e-2) AND (hasAll(f6, 1e10)) +hasAll(f4, ' ') +f1 IN ("a_b%", "a_b%", "xyz%", "%wild%", f6) +() +f3 IN (,) +(f1 BETWEEN 1 AND 1e10) AND (f4 NOT CONTAINS 'test') +hasAll(f1, 0.0000000001) +hasAny(f2, -1e-10) +() +((has(f6, 0.0000000001)) AND (f2 IN (-.5, ' '))) OR ((f3 == f1) AND (f3 ILIKE "hello world")) +(f6 > 'O''Reilly') OR (f6 IN (' ', 0, 6.02e23, 'O''Reilly', TRUE)) +f6 == "\n" +f1 = +f6 === 'O''Reilly' +f2 != 'O''Reilly' +f1 IN (-3.5e-2) +hasToken(f3, 999999999999) +f3 === -10 +f2 IN (-3.5e-2) +f5 CONTAINS "%wild%" +f4 IN ("\t", f6, -1e-10) +f2 BETWEEN -1 AND 'test' +f4 > -1e-10 +has(f5, f4) +f1 = NULL +f5 BETWEEN -.5 AND "hello world" +NOT +f6 IS NULL +f2 ILIKE 'test' +f5 IN (-3.5e-2, 3.14) +f3 BETWEEN 0 AND "\t" +f6 REGEXP "%wild%" +f5 ILIKE ' ' +f1 IN (TRUE, ' ', -3.5e-2) +f6 NOT LIKE 'test' +f6 BETWEEN -10 AND 6.02e23 +f5 = NULL +f6 IN (10, 'O''Reilly', 6.02e23) +f2 IN ("a_b%", "\n", 6.02e23, f1) +f1 REGEXP "%wild%" +f6 = f2 +f4 CONTAINS "abc" +(f3 = 'test' +(f4 BETWEEN f4 AND f4) AND (f6 IN ("xyz%", -3.5e-2)) +f6 IN (1e10) +f6 === -2.7 +(hasToken(f5, f2)) OR ((f5 < -1) OR ((f2 >= ' ') OR (f6 = 'test'))) +NOT +f2 REGEXP ' ' +f3 CONTAINS "\n" +((f1 IN (1, "%wild%")) OR (f3 != f6)) AND ((hasAll(f4, f3)) OR (f1 <= "\n")) +f1 OR OR f5 +f6 IN (,) +f3 OR OR TRUE +f3 = 10 +f1 IN ("\t") +f1 NOT REGEXP '' +hasAny(f3, "xyz%") +(hasAll(f1, f6)) AND (f2 BETWEEN '' AND f5) +f1 <= "\n" +(f5 BETWEEN 1e10 AND 3.14) OR (hasToken(f5, ' ')) +f6 IN () +hasAny(f2, "\n") +(f5 IN ('test', f3)) OR (f1 BETWEEN 0.0000000001 AND 'test') +f4 REGEXP "a_b%" +(f4 = TRUE +f3 >= f6 +f4 LIKE "xyz%" +hasToken(f2, -2.7) +f3 = NULL +(f2 LIKE "xyz%") OR (hasAll(f5, "\t")) +() +f3 IN (1, "\t") +() +f3 = 'unclosed +f6 BETWEEN 0.0000000001 AND -3.5e-2 +NOT +(f5 IN ('test', f5, -2.7, 3.14)) OR (f2 IN (-1, f1, -2.7)) +f5 IN (,) +f1 = 1e10) +hasToken(f4, -3.5e-2) +f4 = 'unclosed +f5 ILIKE 'O''Reilly' +has(f5, -.5) +f1 = '') +((f1 NOT LIKE "hello world") AND (((f6 > 'test') AND (f4 < "\n")) AND (f6 NOT LIKE "a_b%"))) AND (f4 <= ' ') +NOT +f5 OR OR 10 +hasToken(f4, 'O''Reilly') +f3 IN (10, 3.14) +f2 < 6.02e23 +f6 NOT CONTAINS "hello world" +f1 REGEXP "%wild%" +f6 LIKE "\t" +f6 LIKE +f3 <= "%wild%" +f3 LIKE "\t" +f4 <= ' ' +(f4 BETWEEN TRUE AND -3.5e-2) AND (f1 BETWEEN "\n" AND -10) +f1 BETWEEN "abc" AND 1 +f1 IN ("\n", "abc", "xyz%", f5, -1e-10) +f5 = +(f4 = f3 +f1 CONTAINS "hello world" +f2 BETWEEN 1e10 AND '' +(f4 NOT LIKE "abc") AND (f1 BETWEEN "\t" AND "%wild%") +f5 LIKE +hasAny(f5, 10) +f3 IN (3.14, "\t", f4, 1e10, 3.14) +f1 BETWEEN "hello world" AND -.5 +f1 IN () +f4 === TRUE +f6 IN ("abc") +f5 NOT +(f3 IN ("\t")) OR (f3 != -.5) +hasToken(f4, "hello world") +f4 === -3.5e-2 +NOT +has(f6, TRUE) +f5 LIKE '' +f2 > 0.0000000001 +f6 IS NULL +f4 BETWEEN '' AND '' +f4 IN (f5) +f6 ILIKE "\n" +f6 IN (-10, -10, f2, 6.02e23) +f6 IN () +(f4 BETWEEN 10 AND 6.02e23) AND ((f1 != TRUE) AND (f1 <> 0)) +f1 BETWEEN 3.14 AND 'O''Reilly' +(hasAll(f2, -10)) OR (f1 BETWEEN -10 AND -3.5e-2) +f2 NOT LIKE 'test' +f5 BETWEEN f4 AND 1e10 +f1 NOT ILIKE "abc" +f5 BETWEEN f4 AND "\t" +f6 = 0 +f6 NOT CONTAINS "\n" +f6 BETWEEN f3 AND f3 +hasAll(f4, 6.02e23) +has(f5, -1e-10) +f3 LIKE +f3 === -3.5e-2 +hasAll(f6, "%wild%") +has(f1) +hasToken(f6, 'test') +f6 LIKE '' +(f3 IN (f5, f5, "%wild%", "abc")) AND ((f5 NOT ILIKE "a_b%") AND (f3 BETWEEN f4 AND 3.14)) +f6 IN (,) +f1 BETWEEN TRUE AND 3.14 +f6 NOT +f3 IN () +f5 NOT +NOT +f3 BETWEEN 999999999999 AND -3.5e-2 +f3 BETWEEN '' AND .5 +f1 BETWEEN TRUE AND '' +f2 LIKE +f5 NOT ILIKE "\t" +NOT +f2 IN (.5, "\n", f1, "a_b%") +f3 IN (-3.5e-2, "xyz%") +f3 BETWEEN f5 AND f1 +AND f4 = '' +f5 BETWEEN -1e-10 AND "\t" +f6 = -10 +(f2 NOT LIKE "\t") AND (hasAny(f5, 1)) +f1 LIKE +f1 <= -10 +f6 BETWEEN f1 AND "hello world" +f6 IN (,) +f2 NOT REGEXP "abc" +f5 < f3 +hasAny(f1) +(f2 BETWEEN "%wild%" AND 6.02e23) AND (f6 ILIKE "xyz%") +has(f2, "abc") +(f1 != f3) AND (hasAll(f1, 3.14)) +(f6 LIKE "a_b%") OR (f2 BETWEEN 999999999999 AND -1) +f5 NOT CONTAINS "%wild%" +AND f5 = "hello world" +f4 IN (f4, 999999999999) +f1 <> 0 +f4 <= 6.02e23 +(f2 IN (.5)) AND (((has(f3, 0)) AND (f2 > -.5)) AND (f6 NOT LIKE '')) +(f3 <= ' ') OR (f6 NOT ILIKE ' ') +f6 < "abc" +f2 NOT CONTAINS "a_b%" +f3 BETWEEN "abc" AND "a_b%" +f4 IN (-.5, 6.02e23) +(f4 CONTAINS "abc") AND (hasAll(f4, 0)) +f6 != "\n" +f3 BETWEEN -2.7 AND '' +f5 > 'O''Reilly' +f5 IN ("\t", "%wild%", f1, 0, "xyz%") +f3 IN ("a_b%", "\n", f3) +f3 IN () +f3 = NULL +hasAll(f6, -3.5e-2) +f6 BETWEEN -2.7 AND f3 +f1 BETWEEN 'O''Reilly' AND 0.0000000001 +f1 BETWEEN -.5 AND 'O''Reilly' +f1 IS NULL +f6 >= 0.0000000001 +f6 <> "\n" +hasToken(f4, "xyz%") +hasAll(f5, 999999999999) +(f3 BETWEEN "a_b%" AND "abc") OR (f6 IN ("%wild%", .5, "a_b%", "hello world", -2.7)) +f2 = +AND f5 = "\t" +(hasToken(f6, -1e-10)) AND (f3 NOT CONTAINS "\n") +f3 > 1 +f4 <> "%wild%" +f3 IN (0, "a_b%", 1e10) +NOT +f5 NOT +f2 >= '' +((f3 != "%wild%") AND (f6 BETWEEN -1e-10 AND f1)) AND (f5 NOT ILIKE 'O''Reilly') +f1 = "hello world" +has(f5, 'test') +f3 NOT ILIKE 'O''Reilly' +f1 BETWEEN f1 AND TRUE +f2 IS NULL +hasAll(f6) +hasAny(f6, -10) +((has(f1, -1)) AND (hasToken(f4, -1e-10))) AND (f4 >= -2.7) +f6 IN (,) +f2 = NULL +f5 IN (-2.7) +f3 = NULL +hasAny(f3, "a_b%") +f4 IN [1 2 3] +f1 <> 1e10 +f1 = NULL +f3 IN ('', ' ', 10) +(f6 >= "\n") AND (f2 IN (TRUE, -.5, 'test', 'O''Reilly')) +f4 LIKE 'O''Reilly' +(f1 = 10) AND ((f5 IN (-1e-10)) OR ((f3 NOT REGEXP "%wild%") OR (f3 BETWEEN 6.02e23 AND f5))) +f1 BETWEEN f2 AND f5 +(f1 = TRUE +f4 BETWEEN -1e-10 AND "xyz%" +f5 IN (-10, 10, 1, 1, 1) +f1 BETWEEN -10 AND -.5 +f1 CONTAINS "%wild%" +AND f2 = '' +f5 IN (1e10, 3.14, f4) +() +(f4 BETWEEN f4 AND .5) OR (f6 <= 'test') +f6 IN () +(f5 >= "abc") AND (f1 BETWEEN f4 AND -1) +hasAll(f2, -3.5e-2) +f5 IN (,) +f4 IN [1 2 3] +f1 = -10 +f3 = 'unclosed +(hasToken(f4, -1)) AND (f2 BETWEEN "abc" AND '') +f6 BETWEEN "%wild%" AND -3.5e-2 +(hasAll(f6, '')) OR (f2 == f4) +(f1 <> -3.5e-2) OR (f1 NOT CONTAINS 'test') +f3 == "hello world" +f4 BETWEEN ' ' AND f2 +f6 IN () +(hasToken(f4, '')) OR (hasAll(f1, -10)) +f3 IN (f2) +hasAll(f4, 6.02e23) +f4 BETWEEN 10 AND .5 +f3 >= -3.5e-2 +(f2 = "a_b%" +f1 IN (0, "%wild%", "hello world", 0, 1) +(has(f6, "hello world")) AND (f4 <= -2.7) +hasToken(f1, f1) +has(f6, 0.0000000001) +has(f4, f3) +f3 >= "xyz%" +(f4 NOT LIKE "%wild%") OR (f4 LIKE "\t") +f4 NOT CONTAINS '' +f6 <= 1e10 +hasAny(f3, '') +f1 IN ("%wild%") +() +f1 <= f5 +f5 = "\t") +f6 NOT LIKE "%wild%" +f1 IN (-1e-10, -2.7) +hasAny(f2) +f6 IN () +f6 BETWEEN "hello world" AND 10 +hasAny(f4, -1) +f3 >= "\t" +f1 OR OR 6.02e23 +f5 = 'unclosed +f2 NOT CONTAINS "hello world" +f1 = 'unclosed +NOT +(hasAll(f2, .5)) OR (f3 <= f5) +f2 IN (3.14, -10, f1, 999999999999) +has(f5, -1) +f2 LIKE "hello world" +hasAny(f5, 999999999999) +has(f4, f2) +f3 != 'O''Reilly' +f3 IN (3.14, -1) +f2 BETWEEN AND 999999999999 +f6 BETWEEN "%wild%" AND "xyz%" +f5 <> -2.7 +has(f1, "abc") +f2 OR OR -1 +hasToken(f2, ' ') +hasToken(f5, '') +f1 === f6 +hasAll(f4, -3.5e-2) +(f1 = 'test' +hasAll(f5, 10) +f4 = -10) +f2 < .5 +f1 IN [1 2 3] +f4 = +f2 NOT LIKE 'O''Reilly' +f5 != "hello world" +f5 BETWEEN ' ' AND 'O''Reilly' +f4 IN ("\n", 'test', 6.02e23, "\t", .5) +f2 REGEXP ' ' +f2 NOT +f2 >= 'test' +AND f6 = f4 +has(f1) +f6 IN [1 2 3] +f1 NOT ILIKE 'O''Reilly' +f1 CONTAINS "\t" +f6 BETWEEN AND -.5 +f3 OR OR "hello world" +f4 NOT ILIKE "\n" +has(f1, -.5) +f4 <> "abc" +f3 NOT CONTAINS "\n" +f3 CONTAINS ' ' +f4 IN (f5, "\n", "\t", f2) +f5 === 'O''Reilly' +f2 BETWEEN .5 AND f1 +(f3 = .5 +f3 <> "a_b%" +f1 IN (,) +f3 == 0.0000000001 +f2 IN (f3, -1, -1, "a_b%", f3) +f2 BETWEEN AND "xyz%" +f3 BETWEEN "hello world" AND '' +f3 LIKE "hello world" +hasAny(f3, 6.02e23) +f6 > "a_b%" +f1 BETWEEN 6.02e23 AND -2.7 +hasAll(f3, 1e10) +f1 BETWEEN AND f2 +f1 >= 0 +hasAny(f1, 6.02e23) +f5 BETWEEN -1e-10 AND "\t" +f3 IN (.5, f4, 6.02e23, -10, TRUE) +f5 IN (-10) +f4 IN (f5, 1e10, "\n", .5, .5) +f1 ILIKE "xyz%" +hasAll(f3, -1) +f3 LIKE +f6 != 'O''Reilly' +(f4 CONTAINS "hello world") AND (f6 BETWEEN "\n" AND 1e10) +f5 BETWEEN "hello world" AND "xyz%" +f1 NOT LIKE 'O''Reilly' +f6 != 0 +hasAll(f3, "xyz%") +f3 = 'unclosed +f2 OR OR "hello world" +f6 = f2 +f3 ILIKE "a_b%" +f3 BETWEEN "\n" +AND f4 = "%wild%" +(hasToken(f3, "abc")) OR (f4 NOT CONTAINS 'test') +f5 BETWEEN "\t" AND 1e10 +f2 NOT ILIKE '' +f3 IN (1, .5, 3.14, 1e10) +f6 BETWEEN ' ' AND -1e-10 +f2 OR OR 6.02e23 +f3 IS NULL +f4 < 3.14 +f4 IN (-3.5e-2, 6.02e23, "hello world", 'test', -.5) +f1 IN ('O''Reilly', 999999999999, "abc", 0.0000000001) +has(f4, "\t") +f2 ILIKE 'test' +hasAll(f2, f4) +f6 = 'unclosed +(hasToken(f4, '')) AND (f5 REGEXP '') +(hasToken(f6, 'test')) OR (f5 IN (f3, f5)) +(f5 IN (.5)) OR (f6 <> "\t") +(f2 NOT LIKE "xyz%") OR (f1 IN ("xyz%")) +(hasAny(f2, 3.14)) OR (f4 != ' ') +hasAll(f2, "xyz%") +f2 === f6 +f5 OR OR 'O''Reilly' +f6 IN (,) +(f4 BETWEEN .5 AND -3.5e-2) OR (f4 != .5) +f1 BETWEEN 0.0000000001 AND '' +f5 BETWEEN f2 AND "%wild%" +(((f2 IN (-1, '', '')) OR (f2 BETWEEN '' AND -10)) OR (f2 IN (6.02e23, f2))) OR (f2 > 'test') +f3 IN (-.5, -3.5e-2, -1) +((f5 BETWEEN .5 AND 1e10) OR (f2 NOT ILIKE "\t")) OR ((f4 BETWEEN 1 AND f1) AND (f2 BETWEEN 10 AND 0)) +f1 NOT REGEXP "a_b%" +f4 IN () +f6 BETWEEN AND f1 +f2 <= "\t" +has(f4, -.5) +has(f4, f3) +has(f3, "%wild%") +f5 <= "%wild%" +f2 BETWEEN f6 AND f6 +f6 NOT LIKE ' ' +f4 IN (0) +f1 IN ('') +f5 IN [1 2 3] +((f3 NOT REGEXP "abc") AND (has(f4, f1))) OR ((f6 >= -10) AND ((f3 BETWEEN TRUE AND -2.7) AND (f3 IN (1e10, "xyz%", 1)))) +NOT +AND f2 = -1e-10 +f3 BETWEEN "a_b%" AND 10 +f5 IS NULL +hasToken(f1, 0) +f6 IN (TRUE) +f1 IN (0.0000000001) +(f6 == 'test') OR (f4 = "hello world") +hasAny(f5, "a_b%") +f2 ILIKE 'O''Reilly' +f6 IN (-3.5e-2, 6.02e23, 'test') +f1 <> "xyz%" +hasAny(f1, TRUE) +hasToken(f2, '') +f3 BETWEEN 1e10 AND f4 +f6 BETWEEN TRUE AND f6 +f1 CONTAINS "\n" +f5 REGEXP ' ' +has(f2, '') +(f3 NOT LIKE "hello world") AND (hasAll(f6, "xyz%")) +f6 NOT +(f6 != 0.0000000001) AND (f4 NOT LIKE 'O''Reilly') +f6 IN (6.02e23) +f1 == f6 +f5 NOT LIKE "abc" +has(f6, 0.0000000001) +f1 == -1 +f5 < "hello world" +f3 IN (0.0000000001, -.5, ' ') +f1 BETWEEN -10 AND "%wild%" +f1 < '' +(f3 ILIKE "xyz%") OR (f3 IN ('O''Reilly')) +hasToken(f2) +f2 ILIKE "hello world" +f4 IN (-1e-10, 3.14, "hello world", 0.0000000001, ' ') +has(f1, f5) +f4 NOT LIKE "\n" +has(f6, "abc") +f4 NOT ILIKE "xyz%" +f3 BETWEEN -1e-10 AND f3 +f3 IN [1 2 3] +(f3 CONTAINS ' ') AND (f5 BETWEEN f3 AND 3.14) +has(f2, 10) +(f6 IN ('', "a_b%", f3, -1, "xyz%")) AND (f2 IN (f3, TRUE, TRUE, f4)) +f3 BETWEEN "hello world" AND "\t" +f1 ILIKE '' +f4 BETWEEN -.5 AND 999999999999 +f4 IN (6.02e23) +(f5 BETWEEN "\n" AND "\n") AND (f2 REGEXP "hello world") +f6 < -3.5e-2 +f6 IN ('test', "\t", 0) +((hasAll(f5, f6)) AND (f1 BETWEEN -10 AND 0.0000000001)) AND ((f3 IN (-2.7, f4)) OR (f3 BETWEEN ' ' AND TRUE)) +f6 IN ('test') +f3 BETWEEN 1 AND 'O''Reilly' +(f4 <> 3.14) OR (((f3 NOT LIKE ' ') AND (f1 <> 0.0000000001)) OR (hasAll(f1, f2))) +f1 BETWEEN -.5 AND -1 +(f3 = "a_b%") OR (hasAny(f1, -2.7)) +f5 = f2 +AND f3 = 10 +hasToken(f4, f2) +hasToken(f3, 10) +f6 IN (-1) +hasAll(f3, -10) +(f1 IN (f5, -3.5e-2, .5, 6.02e23)) OR (f6 > 6.02e23) +f1 CONTAINS ' ' +hasAny(f4, f4) +f1 <= "xyz%" +f3 IN (3.14, "abc", 0.0000000001, -.5) +hasToken(f1, 999999999999) +f2 = -1 +f4 BETWEEN 6.02e23 AND '' +f5 NOT LIKE "\t" +(((f3 == f5) OR (f3 BETWEEN 0 AND "hello world")) OR (f1 LIKE "xyz%")) AND (f6 NOT LIKE "\n") +hasToken(f1, 3.14) +f6 BETWEEN 6.02e23 AND f1 +f4 NOT LIKE 'O''Reilly' +(hasToken(f1, '')) AND (f4 BETWEEN 'O''Reilly' AND -1e-10) +f2 > -10 +((f5 == -.5) OR (f3 != -10)) AND (f6 <> 999999999999) +f1 BETWEEN -1 AND "\n" +f6 BETWEEN -3.5e-2 AND f6 +hasAll(f6, f3) +(f4 IN (-2.7, 1, 'O''Reilly')) OR (f1 BETWEEN f4 AND -.5) +f3 = 'unclosed +f1 IN (,) +(f5 = 3.14 +f3 BETWEEN "a_b%" AND f1 +f2 LIKE +((f2 NOT ILIKE "%wild%") OR ((hasAny(f1, .5)) AND ((f6 != 6.02e23) AND (f1 == -2.7)))) AND (hasAny(f5, 10)) +hasAny(f5, 0.0000000001) +has(f6, -1e-10) +(f6 <= 1) AND (f5 != TRUE) +(has(f6, .5)) OR (f2 BETWEEN "a_b%" AND TRUE) +hasAll(f2, f6) +f3 >= f5 +has(f4, f3) +f5 LIKE "hello world" +f3 REGEXP "hello world" +AND f6 = "\n" +f3 REGEXP "hello world" +f2 = 'unclosed +hasAll(f1, "\t") +hasToken(f4, -2.7) +f3 BETWEEN "xyz%" AND "%wild%" +f3 NOT +f1 NOT LIKE "xyz%" +NOT +hasAll(f1, f4) +f1 CONTAINS "xyz%" +f2 != 999999999999 +f3 IN [1 2 3] +f3 IN (,) +(((hasToken(f1, 'test')) OR (f6 NOT REGEXP ' ')) OR (hasToken(f2, 999999999999))) OR (has(f6, -1)) +f5 IS NULL +f3 NOT ILIKE 'test' +f5 >= 10 +f1 IN (-1e-10) +f2 IN (f4) +((hasAny(f4, -2.7)) OR (f6 BETWEEN "xyz%" AND 1)) AND (f1 LIKE "xyz%") +hasAny(f3, "\n") +f2 BETWEEN 3.14 AND -1 +hasAll(f6, f4) +(f4 IN (-1, '')) OR (f1 BETWEEN 3.14 AND 3.14) +f5 ILIKE ' ' +f6 BETWEEN 'O''Reilly' AND -1 +has(f5, 'test') +((f5 == f6) AND (f1 NOT REGEXP "xyz%")) AND (f5 < -3.5e-2) +hasToken(f2, TRUE) +f2 IN ("xyz%", f1, 0.0000000001, '', f2) +hasToken(f6, -1e-10) +f4 IN (f3, 'test') +f3 >= 1e10 +(f5 NOT ILIKE "xyz%") AND (f1 ILIKE 'test') +f5 NOT LIKE "\t" +f3 > "hello world" +f2 NOT +f3 OR OR -1e-10 +f4 <> -.5 +f4 IN (,) +f6 >= "%wild%" +(f6 <= TRUE) OR ((hasAny(f2, f5)) AND (f6 LIKE ' ')) +hasToken(f1, 1e10) +AND f4 = "abc" +hasAny(f6, 3.14) +((f3 CONTAINS ' ') OR (f3 IN (-1e-10, 1e10, 6.02e23, "%wild%"))) AND (f3 IN (f3, 'test', f6, 0)) +() +f6 IN (,) +has(f6, .5) +(f6 BETWEEN "hello world" AND -3.5e-2) OR (f6 <= TRUE) +f5 BETWEEN "%wild%" AND 0.0000000001 +f1 IN (6.02e23, 0) +f3 = "%wild%" +f2 BETWEEN 6.02e23 AND "hello world" +f2 != 3.14 +(f5 LIKE "abc") OR (has(f3, TRUE)) +f5 BETWEEN 0.0000000001 AND '' +f4 <> 1e10 +(f1 = 0.0000000001 +f1 = -1e-10) +f3 NOT LIKE "a_b%" +f1 IN [1 2 3] +f1 != "abc" +((f5 BETWEEN -.5 AND "a_b%") AND (f2 IN (0.0000000001, "a_b%", -1, 1e10))) AND (f6 < 3.14) +f1 == "%wild%" +f1 BETWEEN 0 AND "%wild%" +hasAll(f6, 1e10) +() +f4 = 'unclosed +f6 NOT CONTAINS "abc" +f1 IN ('O''Reilly') +hasAny(f5, 1) +(f2 LIKE "\t") AND (hasAny(f5, 1e10)) +f4 BETWEEN '' AND 6.02e23 +(f5 BETWEEN f3 AND -.5) OR (((f6 > "\n") OR (f6 NOT CONTAINS "\n")) AND (f4 NOT LIKE "hello world")) +hasAll(f3, 1) +f3 >= -.5 +f2 BETWEEN -3.5e-2 AND 'O''Reilly' +f5 = NULL +f4 NOT +f1 CONTAINS "%wild%" +f5 BETWEEN "a_b%" +() +f1 NOT REGEXP "\t" +f5 IN (-2.7, f4, 6.02e23, .5) +has(f5, "a_b%") +f2 = -.5) +hasAny(f5, 999999999999) +(f5 > 6.02e23) AND ((f3 NOT REGEXP "\n") AND (f3 BETWEEN "xyz%" AND 10)) +f4 IN ('', 3.14, f4) +(has(f3, "xyz%")) OR (((f6 IN (-.5, 1, '', "hello world", "a_b%")) OR (f5 IN (1e10))) AND (f1 >= 'test')) +NOT +f3 = +f4 IN ("\n", 'test', -10, f1, -1e-10) +f5 BETWEEN -10 AND 0 +(f3 = f6 +f1 <= "\n" +f4 IN (f1, 0.0000000001) +((f3 BETWEEN -1 AND 3.14) OR (f3 LIKE ' ')) OR (f6 IN ('test', 3.14, "xyz%", "\n")) +f4 IS NULL +f6 <= .5 +(f4 = 'O''Reilly' +f3 NOT REGEXP '' +f1 NOT LIKE "abc" +f4 CONTAINS "\n" +f3 BETWEEN TRUE AND 6.02e23 +f2 IN (1e10, f6, "a_b%", 3.14) +f6 BETWEEN 999999999999 AND f5 +f4 IN ('test', -10, f5, 999999999999) +f4 BETWEEN 10 AND .5 +f5 CONTAINS ' ' +(f2 IN ("%wild%", -1, 999999999999)) OR (has(f6, f1)) +(f3 IN (10, "\t", -2.7, f6, 'O''Reilly')) OR (f5 IN ("\n", 0, '', f2)) +() +f4 = 0.0000000001) +f4 IN (f3, 0.0000000001, 10, "\n") +f1 IS NULL +f4 IN (TRUE, ' ', f4, .5) +f2 != "\t" +((f3 BETWEEN "xyz%" AND "%wild%") OR (f3 BETWEEN "xyz%" AND 3.14)) OR (f3 ILIKE "%wild%") +has(f6, 1) +f6 IN (f6, -10, "abc", 999999999999) +f4 = 0.0000000001 +(f4 IN (f4, 0, "%wild%", -1e-10)) AND ((f5 > 3.14) AND (hasToken(f6, 10))) +f6 ILIKE 'O''Reilly' +f5 OR OR -1e-10 +f5 BETWEEN 1 AND "xyz%" +f3 IN ("\n") +f6 NOT LIKE 'test' +f4 IN (,) +f1 BETWEEN AND f6 +hasToken(f2, -10) +() +(f5 = "a_b%" +f6 IN (f2, -3.5e-2, 'test') +(f2 CONTAINS 'test') AND (f1 IN (3.14, TRUE)) +f5 CONTAINS 'test' +f4 IS NULL +(f1 > -1) AND (f2 IN (f1)) +f2 BETWEEN "a_b%" AND "\t" +f3 BETWEEN -1e-10 AND "abc" +f1 ILIKE "abc" +NOT +hasAny(f4, f6) +f3 BETWEEN '' AND "\t" +(f1 BETWEEN 10 AND f4) OR (((f1 LIKE "\t") OR (f5 <= f3)) OR (f2 IN (0.0000000001, "hello world", ''))) +f4 = +f1 == "abc" +f1 BETWEEN 1e10 AND -1e-10 +f4 ILIKE "%wild%" +f2 LIKE '' +hasAny(f1, "%wild%") +(f2 IN ("\t")) AND (hasAll(f6, 0)) +f5 IN (-2.7, -10, f1) +hasAny(f6, -3.5e-2) +f3 <> "\n" +f5 IN (0.0000000001, -1e-10, "xyz%", "\n") +f4 CONTAINS '' +f1 BETWEEN 1 AND f5 +f2 >= f5 +f3 NOT ILIKE '' +f5 != '' +f3 < "%wild%" +f3 IN [1 2 3] +hasAll(f1, 'test') +f5 REGEXP '' +f1 BETWEEN .5 AND f1 +f5 = f2) +f1 IN (,) +hasAll(f6, -1e-10) +f5 BETWEEN 0 AND '' +f2 REGEXP ' ' +f6 IN (,) +f5 BETWEEN -3.5e-2 AND '' +f5 IN ('test') +hasAll(f4, "a_b%") +f6 <> -2.7 +f1 IN () +(f3 == f5) OR (f6 IN (1)) +f2 ILIKE "%wild%" +hasAny(f3) +hasToken(f4, "\t") +f4 BETWEEN 1 AND 0.0000000001 +f1 = NULL +hasAll(f3, -2.7) +f1 IN (TRUE, f6, 999999999999, -3.5e-2, f4) +f5 BETWEEN 10 AND f6 +f4 BETWEEN .5 AND 1 +f1 ILIKE 'test' +has(f5, "hello world") +f2 BETWEEN f5 AND 1e10 +f6 BETWEEN f5 AND "a_b%" +NOT +f3 >= f2 +f4 <> .5 +has(f2, "\t") +(has(f4, 3.14)) AND (hasAny(f2, "hello world")) +() +() +f3 > "xyz%" +hasToken(f2) +f4 = 'unclosed +f4 BETWEEN 999999999999 AND -1e-10 +f4 = +f4 BETWEEN 0.0000000001 AND ' ' +f5 != 3.14 +(f3 REGEXP '') AND (has(f4, -3.5e-2)) +f2 IN (f2, 1, -1, .5, 10) +f4 IN (6.02e23, -10, -.5) +AND f3 = 6.02e23 +f4 <= ' ' +f6 CONTAINS ' ' +f4 BETWEEN 10 AND f2 +f5 > '' +f2 IN (-.5, "%wild%", f2, "xyz%", 999999999999) +f4 == ' ' +NOT +NOT +f5 > f4 +f2 NOT LIKE "abc" +NOT +has(f1, -1e-10) +f1 IS NULL +f2 BETWEEN f4 AND f6 +(f2 BETWEEN .5 AND "hello world") OR (f1 != "\t") +(f6 ILIKE "\n") AND (f4 ILIKE 'test') +f1 <> 'test' +f3 >= f1 +f6 BETWEEN f6 AND "\t" +f5 OR OR "\t" +f6 BETWEEN ' ' AND f6 +f2 NOT CONTAINS 'O''Reilly' +hasAny(f6, 'O''Reilly') +hasToken(f1, .5) +f5 BETWEEN .5 AND f5 +f5 BETWEEN -3.5e-2 AND 0.0000000001 +(f3 REGEXP "xyz%") AND (has(f4, '')) +f3 === "%wild%" +f2 <= -1 +f1 = NULL +f4 NOT +f3 IS NULL +f2 BETWEEN "\t" AND ' ' +f3 BETWEEN 'O''Reilly' AND "hello world" +f6 NOT LIKE "hello world" +f4 IN [1 2 3] +f3 BETWEEN f6 AND '' +f6 IN (-.5) +f5 > "abc" +(f1 CONTAINS "\t") OR (f1 <> f5) +hasToken(f4, -.5) +f4 BETWEEN "\n" AND -.5 +f1 > "abc" +f3 < f5 +f3 = "a_b%" +f3 IS NULL +f4 OR OR -2.7 +f5 BETWEEN -2.7 AND f3 +(f5 < f4) OR (f6 IN (-10, .5)) +f3 IN (TRUE) +((hasAny(f5, "abc")) OR (f1 LIKE "\n")) OR (f1 IN (TRUE, -.5, f5, ' ', f5)) +() +(f3 REGEXP "xyz%") AND (f1 <> -3.5e-2) +f3 CONTAINS "hello world" +(f2 NOT ILIKE "%wild%") AND (hasAll(f6, 3.14)) +f2 != "\t" +has(f6, 1e10) +hasAny(f6, 10) +f1 IN (-3.5e-2, f1) +hasToken(f1, f3) +(f3 BETWEEN f3 AND -1e-10) OR (f4 BETWEEN -2.7 AND -.5) +f6 IN (6.02e23, "a_b%") +f1 == 3.14 +f2 <> 1 +has(f5, -10) +f1 BETWEEN -.5 AND "xyz%" +(f3 ILIKE "xyz%") AND (f1 >= "hello world") +f6 > 10 +has(f1, f3) +hasAll(f1, .5) +(f1 IN (-2.7, -3.5e-2, -3.5e-2)) OR (f1 == -3.5e-2) +f4 IN [1 2 3] +hasToken(f1, -2.7) +f4 BETWEEN -2.7 AND 3.14 +f1 IN (.5, "xyz%", "\n") +hasToken(f1, "a_b%") +f1 IS NULL +f4 BETWEEN 1e10 AND 'O''Reilly' +f4 = +f5 BETWEEN AND -10 +f6 = NULL +f4 >= '' +f2 NOT CONTAINS "xyz%" +(f1 < 1e10) AND (f1 BETWEEN f4 AND 999999999999) +() +((f1 CONTAINS "\n") AND (f5 > "abc")) OR ((f3 IN (f2)) OR (hasAll(f1, 'O''Reilly'))) +f2 = 'unclosed +f1 IN (-2.7, 0.0000000001, 0.0000000001, -1e-10, "hello world") +f5 = NULL +f4 IN () +f2 >= f6 +hasToken(f4, -3.5e-2) +f1 BETWEEN 10 AND f2 +f2 = +f4 IN ("\t", 0, "%wild%", 0) +hasAny(f1, '') +(hasToken(f5, -1e-10)) OR ((f1 IN (0, "xyz%", .5, 1)) AND (f1 IN (-2.7, f5, f2, -.5))) +f5 NOT CONTAINS 'test' +f3 <> -1e-10 +NOT +f1 IN (999999999999, 3.14) +f6 REGEXP 'O''Reilly' +f1 = +f5 IN (0.0000000001, f6, 6.02e23, f1, 6.02e23) +f1 BETWEEN AND 6.02e23 +((hasToken(f3, "a_b%")) OR (f6 <= f2)) AND (f1 IN (f1)) +f4 OR OR 6.02e23 +f6 == f5 +f2 > f3 +hasAny(f1, TRUE) +f4 <= '' +hasAny(f1, f3) +f1 IS NULL +f5 IN (.5, f4) +f2 NOT +hasAll(f5, "a_b%") +f1 >= f2 +f4 IN ("abc", "a_b%", f5, 0) +f6 = 'unclosed +(f3 IN (f2, 0)) AND (f2 <= 1e10) +f6 ILIKE "hello world" +f4 NOT LIKE "\n" +f2 = +hasAny(f4, "xyz%") +f1 > f5 +f4 BETWEEN "%wild%" AND -.5 +f1 LIKE "%wild%" +f4 CONTAINS "a_b%" +f3 NOT +f2 CONTAINS 'O''Reilly' +f1 ILIKE "xyz%" +f5 IN (f2, f1, 'O''Reilly', -10, "a_b%") +(f1 BETWEEN "abc" AND -10) AND (f5 BETWEEN "\n" AND "abc") +f6 === f6 +f4 = TRUE) +hasAll(f3, 6.02e23) +f4 BETWEEN f4 AND f4 +f2 ILIKE 'O''Reilly' +f5 > '' +f5 REGEXP ' ' +f6 CONTAINS "abc" +(hasToken(f1, f5)) AND (f1 CONTAINS "\n") +f3 IN (1e10, -1e-10, "\n") +f3 BETWEEN f5 AND -1 +f3 BETWEEN AND -3.5e-2 +f4 = -1 +hasAll(f2, 6.02e23) +(f6 NOT CONTAINS "%wild%") OR (f2 IN (0.0000000001, -2.7)) +(has(f5, '')) OR (f5 < f6) +f3 NOT LIKE "xyz%" +f5 LIKE +hasAny(f5) +f6 >= -3.5e-2 +f1 IN (-.5, -.5, "\t", -10, "\t") +f3 < "\n" +has(f3, .5) +f3 BETWEEN 10 AND 0 +f3 IN ('test') +f3 BETWEEN f2 AND .5 +f4 BETWEEN f5 AND 1 +(f2 BETWEEN f5 AND -.5) AND (has(f2, 6.02e23)) +hasAll(f5) +f4 = 'unclosed +f2 REGEXP 'O''Reilly' +NOT +f6 === 'O''Reilly' +(f4 IN (f5)) OR (hasAll(f2, "hello world")) +f4 <> 1 +has(f6, f6) +f6 OR OR -3.5e-2 +f4 NOT +f1 != 0.0000000001 +f5 NOT REGEXP "xyz%" +f3 IN ('O''Reilly', 'test', "hello world") +f5 BETWEEN f3 AND 0.0000000001 +f5 NOT ILIKE "hello world" +f2 = NULL +f2 <> -10 +f3 NOT ILIKE "xyz%" +f3 NOT REGEXP "%wild%" +hasToken(f4, ' ') +hasToken(f1) +f2 = 'unclosed +has(f3, 10) +f6 BETWEEN "xyz%" AND 3.14 +f4 BETWEEN -1e-10 AND -.5 +((f1 BETWEEN 3.14 AND f3) AND (hasAny(f2, f1))) AND (f5 CONTAINS "\t") +() +f4 IN [1 2 3] +f1 IN [1 2 3] +f3 NOT ILIKE "hello world" +f5 REGEXP '' +f6 BETWEEN -.5 AND 10 +f2 = "\t" +f2 IN (,) +AND f1 = "%wild%" +f3 NOT CONTAINS '' +has(f1, 0) +f1 NOT ILIKE "hello world" +f6 = f6 +f5 = +AND f2 = -2.7 +f1 CONTAINS "abc" +f1 LIKE +f1 IN (,) +f6 BETWEEN AND 6.02e23 +f3 IN (-10, "hello world") +f4 BETWEEN "a_b%" AND "abc" +f3 >= 1e10 +f1 IN ("\n", 6.02e23, -1e-10, 0.0000000001) +f4 = NULL +NOT +(f2 LIKE ' ') AND (f2 ILIKE "\t") +f5 IN ("abc", "\t", 999999999999) +f1 NOT LIKE 'test' +hasAll(f5) +f5 IN (-.5, 999999999999, "hello world", 6.02e23) +f3 BETWEEN -.5 AND 6.02e23 +f1 == "xyz%" +f2 BETWEEN 0 AND '' +f3 NOT CONTAINS ' ' +(f6 != ' ') AND (f3 NOT ILIKE 'O''Reilly') +f5 < "abc" +(f5 = TRUE +hasAny(f1, "\t") +(f3 NOT REGEXP "xyz%") AND (f4 CONTAINS "xyz%") +f6 CONTAINS "\t" +f3 BETWEEN AND f3 +f4 BETWEEN f3 AND 'O''Reilly' +f6 IN (1e10, "%wild%", 'test', "a_b%", f3) +f3 IN (-3.5e-2, -1, 10, -2.7) +(hasAll(f2, -3.5e-2)) AND (f4 BETWEEN -.5 AND "xyz%") +f3 <= '' +f2 IN (f5) +f4 IN ("\n", f5) +has(f2, 10) +(f1 BETWEEN "hello world" AND -1e-10) AND (f4 >= 1) +(f1 BETWEEN '' AND -1e-10) AND (f5 BETWEEN 'O''Reilly' AND 10) +() +f2 NOT LIKE "\n" +f3 IN (10, f1, "\n", f2, "\n") +f2 LIKE "\t" +f6 BETWEEN ' ' +f3 LIKE +f5 BETWEEN f2 AND f2 +f5 = 10) +f3 BETWEEN f4 AND "\t" +f5 BETWEEN .5 AND 1e10 +f3 IN (999999999999, f1) +f2 LIKE 'test' +f1 ILIKE "\t" +() +f4 >= f1 +(f3 <= f1) AND (f3 > "abc") +hasAll(f5, -1e-10) +f3 = 6.02e23) +f3 BETWEEN "%wild%" AND "%wild%" +f1 IN ("xyz%", ' ', -1, -1, f1) +f4 BETWEEN 0.0000000001 AND "hello world" +((f4 <> .5) AND (f2 BETWEEN "\n" AND -1e-10)) OR (f2 IN (1, "hello world", 0.0000000001, -1e-10, 1e10)) +f3 REGEXP 'O''Reilly' +NOT +f3 == '' +f6 IN (f5) +has(f3, f2) +has(f6, f1) +(f2 > f2) AND (f1 ILIKE "\n") +f2 BETWEEN "xyz%" AND f2 +f1 CONTAINS ' ' +f3 LIKE "abc" +f6 = +(f6 LIKE "xyz%") OR (f4 IN (6.02e23)) +f2 NOT REGEXP "%wild%" +f3 === '' +f3 != 6.02e23 +f6 = 999999999999) +f1 BETWEEN AND "hello world" +(f2 NOT LIKE ' ') AND (f1 IN (-.5)) +f5 BETWEEN -1 AND ' ' +(f1 IN (-1e-10, "\t", -3.5e-2, "\n", 999999999999)) AND (f6 BETWEEN 'O''Reilly' AND "xyz%") +f5 NOT LIKE '' +f2 IN ("abc") +f2 IN (-3.5e-2, 999999999999) +f3 != 0.0000000001 +f3 IN (f2, 0, "xyz%", f6) +f2 IN (,) +(hasAll(f3, -10)) AND (hasAny(f6, f3)) +hasAny(f3, -10) +f5 IN ("xyz%") +f5 IN [1 2 3] +f5 >= 10 +f4 === 6.02e23 +f1 OR OR -3.5e-2 +(f4 >= 6.02e23) AND (has(f5, f1)) +f6 IN ("%wild%") +f2 IN (1, 10, 6.02e23) +f1 CONTAINS "xyz%" +f4 NOT REGEXP 'test' +hasAll(f3, 999999999999) +((f1 NOT LIKE '') OR (has(f5, "hello world"))) AND ((hasAny(f3, "abc")) AND (f3 < 6.02e23)) +hasToken(f3, f3) +f4 <= .5 +f4 IN (10) +f5 != 10 +f4 BETWEEN "a_b%" +f3 BETWEEN "xyz%" +f4 NOT ILIKE "\t" +f2 NOT REGEXP "\n" +f1 IN () +f1 == 1e10 +hasAny(f5, "hello world") +f1 NOT ILIKE "\t" +f2 > 1 +(f5 == -3.5e-2) AND ((((f2 >= '') OR (f2 < "a_b%")) AND (f6 IN ("\t", .5, TRUE))) OR (f2 BETWEEN 10 AND 0)) +f2 CONTAINS 'test' +(f1 NOT LIKE "a_b%") AND ((((f4 <> 999999999999) OR (f5 != TRUE)) AND (f6 IN ("\t", "xyz%", ' '))) OR (f3 NOT ILIKE "xyz%")) +f2 <= f1 +NOT +f2 > ' ' +f6 <= ' ' +hasAll(f4, "hello world") +f1 > TRUE +NOT +(f1 IN (-1e-10, -1)) AND (f2 BETWEEN 'test' AND -1e-10) +f6 > -1 +f6 BETWEEN "abc" AND 'O''Reilly' +((has(f1, 'O''Reilly')) AND (f4 >= "xyz%")) AND (hasAny(f2, -.5)) +f1 IN (0) +f6 NOT CONTAINS "abc" +f3 BETWEEN -10 AND -10 +f5 NOT +f4 NOT ILIKE "abc" +f2 NOT LIKE ' ' +(f2 = f6 +hasAny(f4, 'O''Reilly') +f4 NOT ILIKE "\t" +f5 === -2.7 +f4 REGEXP "a_b%" +f3 IN (.5, 999999999999) +hasAll(f1, "\n") +f5 ILIKE 'test' +f6 IN (,) +f3 BETWEEN -1 AND -2.7 +hasToken(f5, -2.7) +f4 IN () +f4 BETWEEN "a_b%" AND 0 +(f1 NOT ILIKE "hello world") AND (f1 IN ("abc", -3.5e-2, -3.5e-2, -1e-10)) +(f6 >= 3.14) AND (f2 = f4) +f4 NOT LIKE "\n" +f1 NOT CONTAINS "\n" +f4 NOT +hasToken(f2, '') +f1 >= 999999999999 +f1 = +f2 NOT CONTAINS 'test' +f4 NOT ILIKE 'O''Reilly' +NOT +AND f1 = 'O''Reilly' +hasAny(f4, f4) +f3 IN (f6, f3, f4, -2.7, '') +hasAny(f6, f5) +f6 != ' ' +f2 BETWEEN -10 AND f5 +f6 IN () +((f1 == -.5) AND (f4 NOT REGEXP "%wild%")) OR (f5 BETWEEN 999999999999 AND f5) +(f2 ILIKE "%wild%") OR (f4 BETWEEN TRUE AND f2) +f1 < -.5 +(f3 = f3 +(f4 REGEXP ' ') OR (has(f4, "a_b%")) +f2 == 1 +f1 ILIKE "\n" +f2 REGEXP "a_b%" +(f1 LIKE "xyz%") OR (hasToken(f1, -10)) +f6 NOT REGEXP "\n" +f3 = NULL +f6 IN (999999999999, 6.02e23, 1e10) +f2 >= "hello world" +f6 OR OR f2 +(f2 BETWEEN 1 AND f4) AND (f6 > "\n") +(f4 REGEXP "xyz%") OR (hasToken(f4, "a_b%")) +f6 IN (f5) +f3 > 1 +f4 REGEXP 'O''Reilly' +(has(f2, 'O''Reilly')) AND (hasAll(f4, -3.5e-2)) +f6 IN [1 2 3] +f6 = "%wild%") +hasAny(f5, TRUE) +f2 IN (-2.7, f3, -10) +f5 LIKE +hasToken(f2, f3) +f4 = +f3 IN (-10, "\n", 'test', -10) +hasAll(f1, -1) +hasAny(f4, 0.0000000001) +((f3 != f3) AND (has(f2, 3.14))) AND ((f3 BETWEEN "%wild%" AND 0.0000000001) OR (f6 BETWEEN -.5 AND 'test')) +f4 CONTAINS "hello world" +() +f1 IN (-10) +(f1 > -1) AND (f1 NOT REGEXP "a_b%") +f3 OR OR -10 +hasToken(f5, TRUE) +f5 LIKE "\n" +(hasAll(f6, 6.02e23)) OR (f3 == f6) +AND f3 = "hello world" +f3 < "\t" +f4 IN [1 2 3] +(hasAll(f4, 1e10)) OR (f4 IN (10)) +f5 BETWEEN 'test' AND 'test' +has(f2) +f6 IN (3.14, -1e-10, "\n", 3.14) +f1 = 999999999999 +(hasAny(f6, .5)) AND (f5 <= f6) +f1 BETWEEN f2 +(hasAll(f6, f5)) OR (f4 IN (-10, 'test', "%wild%", f3, f6)) +f5 = +f2 OR OR 10 +f1 = "hello world") +f4 CONTAINS '' +f6 LIKE "%wild%" +f1 = NULL +f5 IN (' ', .5, "%wild%") +f4 BETWEEN AND -1 +(f4 IN ("%wild%")) AND (((hasAny(f3, f5)) OR (f3 LIKE "%wild%")) AND (f5 NOT REGEXP 'test')) +(hasAny(f4, "\n")) AND (f6 IN (-10)) +f2 IN (,) +f1 BETWEEN ' ' AND 'test' +(hasAll(f4, -2.7)) OR (hasToken(f2, 1e10)) +f4 IN ("abc", -1) +((f1 IN (f4)) OR (f3 > -1e-10)) OR (f2 NOT LIKE ' ') +f2 = "abc") +has(f4, f4) +hasAll(f5, '') +f1 <> f1 +f5 = NULL +f4 NOT +f2 LIKE +hasAll(f1, "xyz%") +f5 BETWEEN 3.14 AND 'O''Reilly' +f1 IN (-.5, f4) +f3 < f1 +f3 BETWEEN '' AND -.5 +f4 IN (,) +f2 NOT CONTAINS '' +f2 = f1) +AND f3 = 1 +f6 IN ("xyz%") +f3 REGEXP "abc" +f3 IN (3.14, -3.5e-2) +(f5 IN (0.0000000001, 6.02e23)) OR (f3 BETWEEN 'O''Reilly' AND ' ') +f2 = 'unclosed +f2 IN (f4, 'test', 1e10, -3.5e-2) +f1 CONTAINS 'O''Reilly' +f3 BETWEEN .5 AND 'test' +f6 IN (-.5, "\n", -1, "a_b%") +hasAll(f3, ' ') +f5 IN (-2.7, 10, 0.0000000001, "hello world", f4) +f3 IN (' ', "%wild%", 999999999999, -2.7, .5) +f1 LIKE +f6 = +(hasAny(f5, 999999999999)) AND (f4 BETWEEN "a_b%" AND 6.02e23) +f1 = f2 +hasToken(f5, -10) +f3 = "\n") +has(f6, "a_b%") +f5 NOT +f5 IN (,) +has(f3, f3) +f2 === 3.14 +f3 BETWEEN 1e10 AND 999999999999 +f1 = +f1 < -1 +hasAny(f5, -.5) +f6 BETWEEN 'O''Reilly' AND f6 +has(f2, f5) +f5 IN (f6, f4, 999999999999, "hello world") +f2 IN ("\t", f4, "\t", f2, f3) +f6 = 'unclosed +f6 < "\t" +f1 != -1e-10 +f4 BETWEEN AND 6.02e23 +f2 NOT REGEXP "\t" +f3 IN ("hello world") +f4 NOT CONTAINS "\t" +hasAll(f6, .5) +f4 BETWEEN f6 AND 0 +has(f5, -1e-10) +f5 CONTAINS 'O''Reilly' +hasToken(f1, ' ') +f3 BETWEEN 1e10 +f2 = f3 +hasAny(f6) +f6 > f6 +f6 IS NULL +f2 IS NULL +f1 IN (-1, 'test', f3, -10) +has(f5, "\n") +(f1 IN (3.14, .5)) OR (f5 BETWEEN TRUE AND f2) +f1 IN () +(has(f6, -3.5e-2)) OR (f5 REGEXP "\n") +f3 NOT REGEXP '' +f5 IN (,) +f6 IN ('') +f2 BETWEEN "abc" AND 999999999999 +(f4 >= 1e10) AND (f2 >= -.5) +f6 BETWEEN AND 'O''Reilly' +f3 IN (-1, 0) +has(f2, '') +f6 BETWEEN AND -1 +f6 BETWEEN f5 AND -10 +() +f6 != -1e-10 +f6 <= 1 +f1 === "\t" +f2 IN (f6) +f3 === f3 +AND f5 = "abc" +f2 === f5 +f6 IN ("%wild%", f2, 1, f4) +f6 < .5 +f1 IN (-10, 'test') +f4 == .5 +f1 = +f1 IN ("hello world", "xyz%") +f1 REGEXP "%wild%" +f1 ILIKE 'test' +(hasAny(f5, 1e10)) AND (f6 BETWEEN "abc" AND "\n") +f4 BETWEEN AND 1 +(has(f5, -3.5e-2)) AND (f3 IN (-.5, "xyz%", "abc")) +f5 IN (,) +f1 = 0) +f6 != "\n" +f6 NOT CONTAINS ' ' +f3 BETWEEN -1e-10 AND -2.7 +f6 BETWEEN 999999999999 AND 'O''Reilly' +f3 IN ('O''Reilly', 1, "%wild%", f2) +f1 NOT CONTAINS ' ' +f4 IN ("\n", "xyz%", "abc") +(f5 CONTAINS 'test') AND (has(f6, "a_b%")) +f5 BETWEEN AND TRUE +f2 LIKE 'O''Reilly' +f1 IN (' ') +f2 NOT LIKE "abc" +hasToken(f5, "abc") +(f3 BETWEEN f4 AND -2.7) OR ((f5 IN ("\t", f4, 0.0000000001, 10, -1e-10)) AND (f5 NOT CONTAINS "hello world")) +f1 IN () +(f6 IN (999999999999, "hello world", "\t", f1)) OR (f6 IN (f4, 10, 3.14, "%wild%", 'test')) +f6 BETWEEN AND -.5 +has(f1) +f2 IN [1 2 3] +((f3 <> 0) AND (f6 BETWEEN f5 AND 10)) OR (f4 BETWEEN "\t" AND 1e10) +f6 ILIKE ' ' +f4 IN (f1) +f1 IN (-10, f1, 6.02e23, 0.0000000001) +f2 IN (' ', '', "%wild%", f2) +f4 = ' ') +f6 = 'unclosed +hasToken(f2, -3.5e-2) +f4 === '' +f3 IN (-.5, "abc", 6.02e23, 0, -2.7) +f1 <= -1e-10 +f6 == f1 +hasAny(f2, "%wild%") +NOT +f3 >= 0 +has(f6, -1e-10) +f5 BETWEEN AND f3 +f2 BETWEEN "abc" AND 'O''Reilly' +((hasAny(f6, "abc")) AND (f2 BETWEEN f2 AND f5)) AND ((f3 BETWEEN 10 AND "a_b%") AND (hasAll(f6, f6))) +has(f5, "a_b%") +AND f3 = ' ' +f2 IS NULL +has(f4, 'test') +f5 = 'unclosed +(f5 NOT ILIKE '') AND (f4 IN (.5)) +f2 >= f1 +f3 = 'unclosed +hasAny(f1, f5) +f4 BETWEEN -3.5e-2 AND 1e10 +f3 IN () +(has(f4, "\t")) OR (f4 = 'O''Reilly') +hasToken(f3, -3.5e-2) +f1 IN () +f2 IN (0, '', 0.0000000001) +f5 IN (999999999999, -1) +f1 >= 0.0000000001 +f3 NOT LIKE 'test' +f4 != "\n" +f6 IN ("abc", 0.0000000001, "\n") +(has(f5, f2)) AND ((f4 BETWEEN -2.7 AND "a_b%") AND (f4 BETWEEN "abc" AND f4)) +(f6 = f3 +f5 BETWEEN -1e-10 AND "%wild%" +f5 NOT LIKE "abc" +f2 IN (,) +f6 <> "%wild%" +f1 REGEXP 'O''Reilly' +f3 >= f5 +f2 IN ("%wild%") +f6 NOT LIKE "xyz%" +f3 CONTAINS "a_b%" +f4 > "abc" +(f4 <= ' ') AND (f2 BETWEEN 10 AND 'O''Reilly') +f6 < -3.5e-2 +f4 IN ("xyz%") +f5 IN (10, -10, -1e-10) +f3 BETWEEN "%wild%" AND 1 +f2 BETWEEN 10 AND 1 +(hasToken(f6, "a_b%")) AND (f5 <> -3.5e-2) +f3 IN ('O''Reilly', 0.0000000001, -2.7, 1) +f6 != 1 +f6 NOT REGEXP '' +f4 IN (f4) +f4 === 1e10 +hasToken(f1, "xyz%") +has(f5, .5) +f1 >= f4 +f3 LIKE 'test' +f6 NOT LIKE "a_b%" +f4 REGEXP "abc" +hasToken(f6, 0) +f1 <> 1 +f5 IN (,) +f3 IN [1 2 3] +f1 IN (1, 'O''Reilly', -.5, TRUE, 1) +f4 IN (10, f5) +hasAny(f3, -.5) +f4 IN ('test', -1e-10) +f6 LIKE +f4 IN (999999999999, -3.5e-2, ' ') +f4 NOT +hasToken(f1, "hello world") +AND f2 = f5 +(((f6 BETWEEN -.5 AND f6) OR (f6 != f1)) AND (f6 > -.5)) AND (f1 LIKE '') +f4 >= f4 +f3 NOT ILIKE "hello world" +f2 NOT CONTAINS ' ' +f6 REGEXP "hello world" +hasToken(f3, -3.5e-2) +f5 = 'unclosed +f6 <= 3.14 +f5 = -2.7 +f3 = +f1 != -1e-10 +f2 CONTAINS 'O''Reilly' +hasToken(f1) +f6 = -10) +hasAny(f4, 0.0000000001) +f1 = 3.14 +f3 IN ('') +f3 NOT +hasToken(f1, 1e10) +hasAny(f3, "abc") +f2 BETWEEN AND "\t" +f2 CONTAINS "%wild%" +f5 OR OR -2.7 +f4 IN ("xyz%", f5) +f3 BETWEEN 999999999999 AND 'test' +hasAll(f2, 6.02e23) +hasToken(f2, 0.0000000001) +f3 IN (,) +hasToken(f5, 0) +f6 BETWEEN f3 AND f5 +f2 IN (,) +(f1 LIKE ' ') AND (f5 LIKE "%wild%") +f3 IN () +f6 REGEXP "hello world" +f4 <= ' ' +f5 IN (,) +f4 CONTAINS "hello world" +(hasToken(f4, "abc")) OR (has(f4, 1e10)) +f1 NOT +f4 != ' ' +f6 IS NULL +f6 <> "\t" +(f6 < f1) OR (f5 NOT ILIKE 'test') +has(f3, 6.02e23) +f5 CONTAINS "\t" +f6 = 'unclosed +hasAll(f4, 3.14) +f2 = -10 +hasToken(f1, f5) +f2 LIKE +f5 IN (f4, -1e-10, f6) +f1 NOT +f4 CONTAINS ' ' +(hasToken(f5, "\n")) OR (f2 IN (-1e-10, f3)) +f1 >= 'O''Reilly' +f2 == -1 +f2 NOT REGEXP 'test' +f3 NOT LIKE 'O''Reilly' +f1 BETWEEN AND -3.5e-2 +(f2 IN (-10, 6.02e23, 0)) OR (f3 IN ("\t", f5, 10)) +f3 NOT CONTAINS "abc" +(f1 = -2.7 +f1 LIKE "xyz%" +f1 BETWEEN -2.7 AND '' +(f4 BETWEEN 1 AND -1) OR (f1 CONTAINS 'test') +f6 IS NULL +hasAll(f1, f5) +f2 BETWEEN -10 AND -1e-10 +f4 NOT REGEXP 'test' +f2 BETWEEN "a_b%" AND "\n" +f1 IN (' ', 3.14, f3, "\t", "abc") +(f4 BETWEEN "\n" AND '') OR (hasToken(f4, f3)) +f6 NOT REGEXP "%wild%" +f6 > TRUE +f1 BETWEEN "\n" AND 999999999999 +f6 LIKE +hasAny(f3, "\n") +f1 = NULL +() +f1 CONTAINS ' ' +f3 BETWEEN 0 AND 3.14 +(f4 = f1 +hasAny(f2, "abc") +f4 BETWEEN f5 AND "abc" +f6 IN (10, 3.14, f4) +f1 != 1 +f2 IN (,) +(hasAny(f5, -.5)) OR (f2 BETWEEN "hello world" AND 1) +f5 IN ("abc", "a_b%", 'test', f1) +f2 BETWEEN "\n" AND f1 +f5 BETWEEN f1 AND "\n" +NOT +f5 BETWEEN AND 3.14 +(f1 NOT ILIKE "xyz%") OR (f4 <> "abc") +f2 BETWEEN 999999999999 AND -1e-10 +hasAll(f5, "%wild%") +AND f3 = ' ' +f4 NOT LIKE 'test' +(f6 LIKE "\n") AND (f1 BETWEEN "abc" AND 'O''Reilly') +() +f6 BETWEEN -.5 AND f1 +has(f1, 1e10) +f1 BETWEEN AND f6 +(f4 = "\n" +f2 == f5 +() +(f2 NOT CONTAINS "a_b%") AND (hasToken(f4, f5)) +f2 REGEXP "\t" +f1 = NULL +hasAll(f6, "abc") +hasToken(f3) +hasToken(f2) +f2 <= .5 +f3 <= 1 +f3 === 1 +f2 NOT CONTAINS "xyz%" +f3 ILIKE "abc" +f6 BETWEEN "%wild%" AND "xyz%" +f6 ILIKE 'test' +f2 IN (f3, 'test', 6.02e23) +has(f5, "a_b%") +f3 CONTAINS "a_b%" +hasToken(f3, '') +(f1 NOT ILIKE "hello world") OR ((hasAll(f4, -2.7)) OR (f4 BETWEEN 0.0000000001 AND ' ')) +has(f2, -3.5e-2) +AND f1 = -1e-10 +hasToken(f5) +f6 LIKE "\n" +f6 NOT REGEXP "\t" +f1 CONTAINS "xyz%" +(f4 == f1) AND (f4 CONTAINS "\n") +f2 IN ("abc", 0.0000000001) +f5 IN (,) +f6 IN (-10, 3.14, f2, 0) +f6 IN (-10, '', -1e-10) +f3 > -10 +(f1 BETWEEN 0.0000000001 AND 6.02e23) AND ((f6 <= f4) AND (f5 IN (''))) +f1 = f5 +f4 <= f6 +f1 IN ('test', 10, f1, 'test') +f3 < 'test' +(hasToken(f2, -.5)) OR (f5 BETWEEN -10 AND .5) +f4 = NULL +has(f3, 0.0000000001) +f3 NOT ILIKE 'test' +hasAny(f2, 3.14) +f4 < "\n" +f4 IN (-.5, 1e10, 'O''Reilly', f3, TRUE) +f2 NOT REGEXP ' ' +f3 NOT LIKE 'test' +(f2 CONTAINS ' ') OR (f1 BETWEEN 999999999999 AND f1) +f4 IN (,) +(f5 NOT CONTAINS "a_b%") AND (f5 < "abc") +(f2 CONTAINS "abc") AND (f3 IN (.5, '', f3, f5)) +f5 < "hello world" +f6 IN ("\t", 3.14, "xyz%", -1, f3) +f1 IN ('test', -2.7, 6.02e23, "\n", "xyz%") +f3 IN [1 2 3] +f6 BETWEEN 10 AND "a_b%" +f2 IN () +f5 IS NULL +has(f1, ' ') +f4 IN (10) +f5 NOT CONTAINS 'test' +(f1 = "abc" +f1 = 1e10) +f6 <= f3 +AND f3 = -1 +f3 IS NULL +AND f3 = f4 +hasAll(f1, 1) +f1 IN (0, '', 999999999999) +f4 IN ('') +(hasToken(f5, "%wild%")) AND ((f4 <> 1e10) OR (f4 IN (f6, "abc", 6.02e23, -.5, -10))) +f1 LIKE '' +hasAll(f1, 10) +f5 IN (f3, ' ', "abc", 999999999999) +hasAll(f2, f6) +f3 IN (-1, TRUE, f5) +hasAny(f1, f6) +f2 BETWEEN -2.7 AND -2.7 +(((f3 NOT ILIKE 'test') OR (f4 NOT ILIKE '')) AND (hasAny(f2, f2))) AND (f6 NOT CONTAINS ' ') +f1 BETWEEN 'O''Reilly' AND 1e10 +has(f3, f2) +(f1 = f1 +f6 BETWEEN 1 AND 3.14 +f3 === -10 +(f6 NOT CONTAINS 'test') AND ((f6 IN ("xyz%")) AND (((f3 == .5) OR (f6 = 1e10)) AND (f6 BETWEEN 999999999999 AND 3.14))) +(f6 = TRUE) AND (f3 BETWEEN 6.02e23 AND 10) +f5 BETWEEN 0.0000000001 +f2 LIKE "xyz%" +f5 IN ('', TRUE, -1e-10) +f3 IN (0, '', 0.0000000001) +f3 CONTAINS '' +f3 IN ('') +f4 >= 1e10 +hasAny(f4, 6.02e23) +f6 BETWEEN 'test' AND TRUE +hasAll(f3, TRUE) +f2 ILIKE ' ' +f6 >= "\t" +((f6 REGEXP "\t") AND (f1 < f3)) OR (f5 NOT REGEXP 'test') +f1 NOT CONTAINS "xyz%" +f5 < f4 +(f6 > "\n") AND (f6 BETWEEN 'O''Reilly' AND f3) +f2 == 'test' +f4 BETWEEN -2.7 AND 10 +f6 BETWEEN "\t" +(f3 ILIKE "hello world") AND (f5 LIKE "%wild%") +f3 NOT LIKE '' +f1 BETWEEN -10 AND "\t" +(hasAny(f3, f1)) AND (f4 BETWEEN 3.14 AND -2.7) +() +(f6 IN ('O''Reilly')) OR (f2 NOT ILIKE '') +f4 LIKE "\n" +f5 <= f3 +f6 <= "hello world" +f3 <= 10 +(has(f5, 6.02e23)) AND (f2 ILIKE "hello world") +(f5 BETWEEN f2 AND f6) AND (f2 < f5) +f5 <= f5 +f5 IS NULL +() +f6 NOT REGEXP "abc" +f1 BETWEEN -1e-10 AND 999999999999 +() +hasAny(f5, f5) +f6 BETWEEN -3.5e-2 AND 'test' +hasAny(f1, "\n") +f4 BETWEEN "hello world" AND ' ' +(f6 NOT ILIKE "\n") OR (f5 == -.5) +hasAny(f3, f5) +hasToken(f3, 1e10) +f4 = NULL +f6 LIKE +f1 = 'unclosed +f4 = -2.7) +f3 IN ("xyz%", "a_b%") +f6 BETWEEN f3 AND -.5 +(f2 IN (f2, -.5, -1, 3.14, 'test')) AND (hasAny(f2, f4)) +f3 BETWEEN "\n" AND TRUE +f3 BETWEEN .5 +f3 NOT LIKE "xyz%" +f2 > '' +f5 = 10 +(f4 IN (f4, 1e10, -2.7, -1e-10)) OR (f1 < 999999999999) +f6 <= "abc" +hasToken(f2, -.5) +(f2 = -10 +f6 == f1 +f5 LIKE "abc" +f2 NOT LIKE "a_b%" +f6 LIKE "abc" +f5 IN (3.14, f5, 10, 1e10) +(f3 IN (0, -2.7, f2)) AND (f6 > 1e10) +f2 IN (-.5, ' ') +f2 IN ("xyz%", -3.5e-2) +f2 BETWEEN 3.14 AND 1e10 +f1 BETWEEN 6.02e23 AND "%wild%" +(f6 BETWEEN 1 AND 999999999999) OR ((f2 BETWEEN -.5 AND f5) OR ((f3 >= "xyz%") OR (f6 NOT ILIKE "abc"))) +(f1 = f2) AND ((f4 CONTAINS ' ') OR (f2 IN ('test', -1e-10))) +f6 IN (f6, 'O''Reilly', 0.0000000001, f3) +f6 == f6 +f6 = NULL +f1 IN (1, "a_b%", 10, 0.0000000001) +(f1 <= "abc") OR (hasAll(f3, f2)) +f4 BETWEEN -3.5e-2 AND ' ' +hasAny(f2) +f4 BETWEEN -.5 AND f2 +has(f4, -.5) +f6 IN (1) +f3 NOT LIKE "xyz%" +f5 BETWEEN "abc" AND "xyz%" +NOT +f1 NOT ILIKE "abc" +hasAll(f2, 'O''Reilly') +f1 BETWEEN 999999999999 AND 'test' +f2 BETWEEN 3.14 AND "abc" +f2 = 'unclosed +f5 BETWEEN -1e-10 AND 0 +hasAny(f4, 1) +f3 ILIKE "abc" +(f3 IN (999999999999, 3.14, f6, f1)) AND (f6 ILIKE 'test') +f1 = +f5 IN (-.5, f5, TRUE, "abc") +(f4 NOT ILIKE "\t") AND (f2 IN ("abc", f2, "\n", f5)) +f3 OR OR 0 +hasAny(f5) +hasAll(f4, -3.5e-2) +f3 = +AND f6 = '' +f1 < 3.14 +hasAll(f6, 3.14) +NOT +f5 BETWEEN '' AND -1 +hasAll(f5, -3.5e-2) +() +f6 IN (999999999999, "\n", 6.02e23, 6.02e23) +f4 IN (f1, "%wild%") +f3 >= f2 +f3 BETWEEN ' ' AND 3.14 +f2 IN [1 2 3] +f4 IN (f2) +f4 BETWEEN 10 AND TRUE +f3 IN (0, f1, f5, 'O''Reilly') +((f1 ILIKE "\t") OR ((f6 >= -10) AND (f4 < f6))) AND (f2 BETWEEN 6.02e23 AND ' ') +has(f4, 10) +f5 IN () +(f5 BETWEEN TRUE AND 10) OR (hasAny(f1, 1e10)) +NOT +() +f5 NOT CONTAINS "\t" +f3 IN (f2, .5, 3.14, f4, -.5) +hasToken(f1, 1) +f2 = 'unclosed +f6 NOT LIKE 'test' +f6 IN (0.0000000001, "\t", 'O''Reilly', 10, f4) +f1 BETWEEN 1e10 AND 6.02e23 +hasAll(f6) +f2 NOT +f5 IN ("%wild%", "xyz%") +f3 BETWEEN AND .5 +((has(f2, ' ')) AND (f2 < -10)) OR (hasAny(f3, -1e-10)) +f2 IN ("abc", ' ', -.5, f2, 10) +f4 NOT LIKE 'O''Reilly' +f4 REGEXP "hello world" +hasToken(f1, -1) +f3 BETWEEN 1e10 AND 1e10 +f4 >= 6.02e23 +f6 BETWEEN -1 +f4 BETWEEN AND 0 +has(f3, -3.5e-2) +f6 < -1e-10 +f3 OR OR f4 +f4 BETWEEN f1 +f3 IN ("\n", 0, TRUE, -3.5e-2) +f4 BETWEEN f5 AND 'O''Reilly' +has(f6, f4) +hasAll(f1) +NOT +has(f2) +f5 IS NULL +f3 REGEXP "abc" +f6 > f4 +(f3 = "\n") AND (f1 LIKE "a_b%") +hasAny(f4, ' ') +f3 IN (,) +hasAny(f2) +f5 BETWEEN 10 AND "\n" +(f4 = 10 +f3 > 1 +f1 REGEXP "abc" +hasToken(f4, .5) +f4 BETWEEN AND 10 +f1 < -.5 +f5 NOT REGEXP "abc" +hasAll(f5, f3) +hasAny(f5, "\n") +has(f3) +f2 BETWEEN 999999999999 AND -3.5e-2 +f2 IN ("a_b%", "a_b%", f5, -.5) +f3 != "a_b%" +f5 >= 6.02e23 +hasAll(f4, f2) +f6 NOT CONTAINS '' +f5 = NULL +f4 <= 999999999999 +f4 IN ("\n", ' ', f2, 'O''Reilly', f6) +f5 = +hasAll(f1, 10) +has(f5, 3.14) +f1 BETWEEN ' ' AND 0 +f6 IN (999999999999, '', -1e-10, 'O''Reilly', "a_b%") +hasAny(f3, "%wild%") +f3 == f2 +f3 = NULL +hasToken(f1, ' ') +f6 BETWEEN '' AND 6.02e23 +(hasToken(f2, 10)) OR ((f5 IN (1, "xyz%", 0, -10, "abc")) OR ((has(f5, f6)) OR (f6 BETWEEN 1e10 AND "hello world"))) +f3 IN () +f5 BETWEEN "\t" AND "%wild%" +(((f1 BETWEEN 10 AND f1) AND (hasAll(f4, -.5))) AND ((f6 >= 0.0000000001) OR (has(f1, -1e-10)))) AND ((f4 IN ('O''Reilly')) OR (f3 != 6.02e23)) +f1 BETWEEN 1e10 AND "%wild%" +hasAll(f5, "a_b%") +f2 BETWEEN AND 0.0000000001 +(f3 BETWEEN .5 AND "hello world") AND (f2 IN ('', "a_b%", TRUE, .5, f3)) +f1 = 'unclosed +f1 > 1e10 +f2 NOT CONTAINS 'O''Reilly' +f1 < "hello world" +has(f5, ' ') +f5 < 'test' +f1 != "%wild%" +f5 = +f2 >= "abc" +f3 = -2.7) +(hasAll(f6, 10)) OR (f5 <> -1e-10) +hasToken(f5, ' ') +f1 IN (-.5) +(f6 < "\t") OR (f5 IN (999999999999)) +f1 BETWEEN "\t" AND -10 +(f1 == f2) OR (hasToken(f5, "a_b%")) +f4 IN (0) +f4 IN () +f5 IN (-1, 1e10, -1e-10, 0.0000000001, TRUE) +f6 BETWEEN AND 'O''Reilly' +f5 CONTAINS "a_b%" +f5 BETWEEN "abc" AND -10 +f6 BETWEEN "a_b%" AND f6 +f2 NOT +f4 <= 3.14 +f2 < 1 +f6 IS NULL +f5 BETWEEN "abc" AND .5 +f1 BETWEEN AND "\n" +f2 IN ("%wild%", f6, "\t", 10) +f1 IN (-1e-10, -1e-10, 0.0000000001, 1, 6.02e23) +f5 ILIKE "hello world" +f5 BETWEEN 'test' AND "%wild%" +AND f5 = 999999999999 +f2 LIKE +f1 BETWEEN "a_b%" AND -1 +f3 IN ("abc") +hasToken(f6, 1e10) +f6 BETWEEN TRUE AND 'test' +f4 LIKE ' ' +f3 IN (1e10, -10, ' ', ' ') +f3 ILIKE "xyz%" +f5 > f5 +has(f3, 0) +f5 IN (f1) +(hasAny(f6, 6.02e23)) OR (f6 <= f1) +(has(f4, 0)) OR (f5 IN (0, 0, TRUE, 3.14, -1)) +f2 == '' +f4 NOT REGEXP "hello world" +f2 NOT REGEXP "\n" +f2 IN (,) +has(f5, "a_b%") +NOT +(f2 BETWEEN .5 AND -3.5e-2) AND (f4 BETWEEN 999999999999 AND TRUE) +f3 > 1e10 +f2 = NULL +(f2 IN ('test', -1e-10, 0, f4, -10)) OR (f4 LIKE "\n") +(f6 < 10) AND (f2 BETWEEN -2.7 AND "abc") +f6 LIKE "a_b%" +f6 BETWEEN 10 AND f2 +f3 <= f5 +f4 = +f6 IN [1 2 3] +f3 OR OR ' ' +f6 IN (0.0000000001, -3.5e-2, 1e10, -10) +hasAny(f1, f6) +f1 IS NULL +f4 > "\t" +f6 IN (f2, "\t", f1, .5, 6.02e23) +f4 REGEXP 'O''Reilly' +f4 IN (f2, 'test', 999999999999) +f4 = +f2 BETWEEN "hello world" +f1 IN (-2.7, 'O''Reilly', 3.14, -1e-10) +f4 IN (6.02e23, f5, 'O''Reilly') +(f4 IN ("a_b%", "hello world", 999999999999)) AND ((hasAny(f1, 1e10)) OR (f1 IN ("xyz%", f2))) +hasToken(f2, 0.0000000001) +f6 = ' ' +f6 BETWEEN 3.14 AND 1e10 +f2 IN () +f5 = 'unclosed +f6 = +f6 == "hello world" +f3 = 'unclosed +(f2 IN (f4, f1, 'test', -3.5e-2, f6)) OR ((has(f3, 6.02e23)) OR (f4 > "\t")) +f2 LIKE +has(f1, -2.7) +f4 IN (,) +f2 IN (f3) +f1 BETWEEN f5 AND "abc" +f1 NOT LIKE "%wild%" +f4 != "a_b%" +f1 >= f4 +f6 NOT CONTAINS "%wild%" +(f6 <= "\n") AND (f3 NOT REGEXP '') +NOT +f3 IN (f6, f1) +(f1 IN (TRUE, "%wild%")) AND (f4 CONTAINS ' ') +() +(f2 = '' +f5 IN (TRUE, f5, f5) +f3 IN (f6, 1e10, f1, "abc") +f2 IN ('', 1e10) +hasToken(f6, 0) +f3 BETWEEN "\t" +f1 ILIKE "xyz%" +f1 IN ("hello world", f6, f3, "xyz%", 6.02e23) +f6 IN (f3, .5, -3.5e-2) +(f4 BETWEEN '' AND f3) AND (f6 BETWEEN 1e10 AND -1) +f1 <> f1 +f2 IN (f4, 'test', 0, -3.5e-2, -1e-10) +f2 IN ("a_b%", "xyz%") +hasAny(f4, 999999999999) +f6 BETWEEN 'test' +f1 NOT ILIKE "\n" +((f6 NOT REGEXP "xyz%") AND (f2 IN (-.5, "a_b%", "xyz%"))) OR (f5 IN (999999999999, -1e-10, "abc", "%wild%", f4)) +f5 NOT REGEXP 'test' +f5 IN ("hello world", 1, "hello world", f2, f3) +f4 IN (,) +f6 < "\n" +f1 IS NULL +f3 BETWEEN '' AND -3.5e-2 +f4 NOT +(f1 BETWEEN 3.14 AND -.5) AND ((hasToken(f3, "abc")) AND (f5 BETWEEN -2.7 AND f4)) +f6 = +(f5 = .5 +(hasToken(f6, "%wild%")) AND (f6 IN (TRUE, "xyz%")) +has(f4, ' ') +f2 IN (-1) +f2 = NULL +f5 <> 1 +f3 IN (10, 0, f5, f3) +f6 >= f1 +f3 REGEXP "\n" +f3 IN (0.0000000001, "abc", "xyz%", f6, "\n") +f4 IN (10, 0, -.5, 1e10, 0) +f4 REGEXP "%wild%" +f6 >= '' +hasToken(f3, -.5) +(f2 IN (0.0000000001, 'O''Reilly')) AND (f5 BETWEEN "abc" AND 'test') +NOT +f2 IN (-10, "\n", -2.7, -1e-10, "xyz%") +hasAny(f2, -.5) +f1 CONTAINS 'test' +f5 IN (.5) +f3 BETWEEN .5 AND 1e10 +(f4 = f1 +(f5 = 1e10 +((hasToken(f6, 10)) AND ((hasAll(f5, 999999999999)) OR (f4 == f5))) OR (f2 BETWEEN f6 AND -.5) +f1 IN ("abc") +f3 NOT CONTAINS "\n" +has(f1, "abc") +f2 BETWEEN "xyz%" AND 0 +f6 BETWEEN -3.5e-2 AND 3.14 +(f6 IN (999999999999, -10, 6.02e23)) OR ((f3 LIKE "abc") OR (f3 <> f2)) +f1 IN ('') +(f1 CONTAINS 'test') OR (f3 == -1e-10) +f4 IN (0.0000000001, '') +f5 NOT LIKE '' +f1 BETWEEN TRUE AND f5 +f5 BETWEEN TRUE AND -10 +f3 IN (3.14, 'test', 'test', -.5, "abc") +f6 BETWEEN -2.7 AND "a_b%" +f2 BETWEEN -3.5e-2 AND 'O''Reilly' +f4 IN () +f2 > f2 +f5 = NULL +f5 BETWEEN 999999999999 AND f1 +hasAll(f3, f5) +f1 BETWEEN 999999999999 AND "a_b%" +f3 == .5 +f3 = 'unclosed +(hasAny(f2, "a_b%")) AND ((hasToken(f2, .5)) AND (f1 IN ("\t", 1e10, "%wild%", -2.7))) +f2 IS NULL +has(f6) +(f5 = -3.5e-2 +f3 IN ("a_b%") +f1 BETWEEN "%wild%" +f1 <> "xyz%" +f2 IN (0.0000000001, -2.7) +f5 BETWEEN -.5 AND 6.02e23 +f2 NOT +f1 = +((f2 <= 'test') AND (f6 != ' ')) OR (f4 < "\n") +f4 NOT REGEXP "xyz%" +f6 NOT CONTAINS "a_b%" +f2 NOT +has(f2, 1) +f5 IN (999999999999) +f2 IN (,) +f3 IN ("\n", f1, -1e-10) +(f4 NOT REGEXP '') AND ((has(f3, "\t")) OR (f3 IN ('', -10, "%wild%", f3))) +(f6 BETWEEN 0.0000000001 AND 10) AND (f5 BETWEEN f6 AND -1e-10) +f3 === 'test' +f6 BETWEEN AND f6 +f3 <= f4 +f2 <= "xyz%" +NOT +f6 BETWEEN f6 AND f1 +hasAny(f4, 'test') +f5 IN (-1e-10, '', f4, -.5) +((f2 IN (-1, '', f3, 'test')) AND (f4 IN (.5, 1, -1))) AND ((f3 IN ('', -10, 1e10, "xyz%")) OR (f2 NOT CONTAINS "\t")) +has(f3, 10) +f6 <> -3.5e-2 +AND f5 = 10 +f5 == '' +f4 IN (-3.5e-2, 1e10) +AND f2 = -.5 +(hasAny(f5, -3.5e-2)) AND (has(f5, -.5)) +f3 BETWEEN -1e-10 AND -1 +f6 < "%wild%" +(f4 BETWEEN f6 AND ' ') OR (f1 BETWEEN -.5 AND 999999999999) +hasAll(f3, f4) +f3 <= f4 +((f5 != .5) AND ((f1 <> -3.5e-2) OR (f1 IN (.5, f1)))) OR ((f3 NOT ILIKE 'O''Reilly') AND (has(f5, ''))) +(f4 = -2.7 +has(f3) +f5 = ' ' +f6 <> 10 +f2 IN ("a_b%", "%wild%", '') +f2 LIKE "a_b%" +f1 IN [1 2 3] +f5 BETWEEN TRUE AND f3 +has(f4) +f6 BETWEEN -3.5e-2 AND "abc" +f2 IN (f5, "xyz%") +f6 NOT REGEXP "%wild%" +f4 BETWEEN f4 AND 'O''Reilly' +f2 === 0.0000000001 +f2 BETWEEN AND f5 +f5 IN () +f5 == TRUE +f3 === '' +(f3 BETWEEN 6.02e23 AND "hello world") OR (f6 == "a_b%") +f2 IS NULL +f5 = 'unclosed +f5 BETWEEN AND TRUE +f4 IN ("hello world", -3.5e-2, 'test', "hello world") +(f5 BETWEEN 3.14 AND f6) AND (f3 BETWEEN '' AND 3.14) +f4 LIKE +has(f6, "\t") +f2 BETWEEN "\t" AND 0.0000000001 +f1 IN (1e10) +f3 = 999999999999 +f4 <= 0 +has(f4, -1e-10) +f4 NOT +f2 IN [1 2 3] +f1 BETWEEN -10 AND 6.02e23 +has(f3, "a_b%") +f3 IN (.5, f6) +f6 > -1 +f2 BETWEEN -10 AND TRUE +f6 = NULL +f5 BETWEEN "abc" AND "xyz%" +f4 IN (,) +f1 = +f5 NOT +has(f6) +f5 IN (,) +f2 IS NULL +f2 BETWEEN ' ' AND "xyz%" +hasAny(f3, -1e-10) +hasAll(f6) +has(f5, -3.5e-2) +f4 IN (1e10) +hasToken(f1, "\n") +f5 IN (TRUE, f4, "\n") +f4 BETWEEN f6 AND ' ' +f2 >= 10 +f5 <> TRUE +f6 BETWEEN f5 AND 0 +f6 NOT +f2 IN (TRUE, -10) +f3 IN () +f1 = -2.7 +hasAll(f1, -1) +(((f4 < "a_b%") AND ((f5 == -1) OR (f6 <> "%wild%"))) OR (f5 >= 6.02e23)) OR (f3 BETWEEN "hello world" AND ' ') +f5 IN ('test', f5) +f6 BETWEEN "a_b%" AND "xyz%" +f2 ILIKE "abc" +f6 == "\n" +f4 BETWEEN 'O''Reilly' AND f1 +f4 NOT LIKE "\n" +f5 BETWEEN f3 AND "\t" +f3 NOT REGEXP "a_b%" +f5 REGEXP 'test' +(f4 BETWEEN "\n" AND f2) AND (f4 != ' ') +f2 BETWEEN 0.0000000001 AND "\t" +has(f6, f4) +f6 BETWEEN f3 AND "%wild%" +f6 BETWEEN f3 AND -.5 +f2 == "a_b%" +f2 LIKE ' ' +f5 BETWEEN "xyz%" AND f4 +(f3 IN (f2)) OR (f2 IN (999999999999, "hello world", 3.14, -2.7, 1e10)) +AND f4 = TRUE +f4 < f3 +(f6 IN (0.0000000001)) OR (f1 IN (TRUE)) +f4 IN ("hello world") +f5 OR OR 1e10 +hasToken(f3, "xyz%") +f3 NOT ILIKE "\n" +f6 LIKE +f4 REGEXP 'O''Reilly' +has(f4, "%wild%") +() +f6 IN (0) +(f6 >= f1) AND (f6 BETWEEN "xyz%" AND '') +f6 >= -1e-10 +f2 IN ("hello world", 6.02e23, 3.14, 6.02e23) +f2 BETWEEN 10 AND 'test' +has(f1, 10) +f6 NOT REGEXP 'test' +f1 IN (TRUE, 1) +f4 BETWEEN f2 AND "hello world" +has(f4, "xyz%") +f4 != 'test' +f5 ILIKE "\t" +f2 = +has(f3, 0.0000000001) +f1 IN (f3, .5, "\t") +f1 IN (,) +f6 IN (f2) +f2 <> ' ' +f2 NOT REGEXP 'O''Reilly' +f4 = 0) +f2 LIKE +f4 NOT LIKE ' ' +f2 OR OR -3.5e-2 +f5 IN ("%wild%", 1, .5, 1e10, 0) +f1 = -3.5e-2 +has(f6, 0) +f5 = f2 +hasAny(f3, 'O''Reilly') +f5 BETWEEN -1 AND f1 +f6 NOT CONTAINS "xyz%" +() +f6 = 'unclosed +f3 ILIKE "hello world" +f5 NOT ILIKE "abc" +(f3 > 999999999999) AND (f2 BETWEEN -3.5e-2 AND 3.14) +f1 <> "\n" +f6 = "%wild%") +hasAny(f3, TRUE) +f3 CONTAINS "hello world" +hasAny(f5, "abc") +f1 BETWEEN "xyz%" AND 'test' +f6 = NULL +f5 LIKE "xyz%" +(f4 = "xyz%" +f1 != -1e-10 +(f3 = f4) OR (f3 != 3.14) +f3 < "abc" +f2 >= -2.7 +(hasToken(f6, 6.02e23)) AND (has(f1, -1)) +hasAll(f3, 3.14) +f5 BETWEEN 6.02e23 AND .5 +AND f4 = f1 +f1 CONTAINS "xyz%" +(f5 <= 1) OR (f2 > -1) +f6 = f3) +f3 BETWEEN f3 AND 1 +f4 BETWEEN -1e-10 AND f4 +f6 NOT +f1 BETWEEN "abc" AND 999999999999 +f5 IN (0, -10) +f5 IN (10, f2, -10, -.5) +f1 IN (f3) +f6 IN (-1e-10) +hasAll(f4, "\t") +((f1 >= "\n") OR (f2 >= 3.14)) AND (f1 NOT REGEXP 'O''Reilly') +hasAll(f5, "\t") +f1 <> -1e-10 +f1 = "abc" +(f5 BETWEEN -1e-10 AND .5) OR (f6 IN (6.02e23, 3.14)) +has(f5, 3.14) +(hasAny(f1, "\n")) OR (((f2 IN (6.02e23, 10, "\n", "abc")) OR ((f2 < -10) OR (f6 != f5))) AND (f1 BETWEEN 3.14 AND "a_b%")) +f3 BETWEEN 10 AND 'test' +f3 NOT +f1 NOT REGEXP 'O''Reilly' +f2 NOT +f6 BETWEEN 1e10 AND 1e10 +f5 NOT LIKE "abc" +f5 < 10 +(f4 >= -3.5e-2) AND (f4 BETWEEN "abc" AND '') +f4 <> f6 +f2 IN (999999999999, '', f1) +has(f3, ' ') +(hasToken(f3, -2.7)) OR (f3 IN (TRUE, -1e-10, f5)) +f1 >= -2.7 +f2 = 0 +f1 IN (TRUE, -2.7, 1e10, -3.5e-2, "\n") +f6 = f2) +f2 IS NULL +f2 BETWEEN 0 AND "abc" +f6 LIKE +AND f6 = "hello world" +f1 BETWEEN '' AND -10 +has(f5, "%wild%") +f4 = +f3 > -.5 +f5 = NULL +f3 NOT REGEXP "hello world" +hasAll(f5, f2) +f5 BETWEEN .5 AND "\n" +f4 REGEXP "xyz%" +f6 IN (f3, 1, -10, ' ', -.5) +NOT +f3 IN [1 2 3] +(hasToken(f6, "abc")) AND (((f3 BETWEEN 0 AND ' ') AND (f2 >= "xyz%")) AND (f2 CONTAINS "\n")) +f1 BETWEEN "%wild%" AND f1 +f3 <> 6.02e23 +f6 OR OR "\t" +f2 >= "\t" +(f6 CONTAINS '') AND (f5 BETWEEN .5 AND 1) +f5 >= 'O''Reilly' +hasAny(f5, TRUE) +(f6 <> -10) AND (hasToken(f3, -3.5e-2)) +f2 IN (1, f6) +f4 === 10 +f2 NOT CONTAINS '' +f1 OR OR "abc" +f4 NOT CONTAINS 'O''Reilly' +f6 IN ('') +f5 BETWEEN TRUE AND 3.14 +f5 = f4) +(hasToken(f4, 6.02e23)) AND (f1 IN (f3, f6, "\t", f6, -1)) +hasToken(f5, f6) +f3 BETWEEN "\t" AND f2 +(f5 > 'test') AND ((f1 NOT CONTAINS "%wild%") AND (f5 CONTAINS ' ')) +f5 NOT +f5 IN (f6, 0.0000000001, -10, "hello world", -1e-10) +f5 BETWEEN AND ' ' +hasToken(f6, '') +f2 BETWEEN f6 AND -1 +f5 == f5 +f1 IN (f1, f3, "%wild%", TRUE, "\n") +f1 NOT CONTAINS "\t" +(f6 IN (f3)) OR (has(f4, "abc")) +hasAll(f2, f4) +(f1 IN ('O''Reilly')) OR (f5 > 10) +f1 NOT ILIKE "abc" +f6 = "a_b%") +hasAny(f4, 'test') +f6 IS NULL +f2 <> 10 +(f1 = f1 +(f4 >= TRUE) OR ((f1 NOT REGEXP ' ') OR (f1 BETWEEN ' ' AND -1)) +has(f1, -1) +(f1 CONTAINS "hello world") AND (f1 = "\n") +f5 = 6.02e23) +f2 REGEXP "a_b%" +f1 IN (-1, f4, 'test') +AND f6 = 6.02e23 +f3 NOT ILIKE "a_b%" +f3 BETWEEN TRUE AND .5 +f5 IN (,) +f4 ILIKE "xyz%" +f6 IN (f6, 'O''Reilly', "hello world", f4, 3.14) +((f6 NOT LIKE ' ') OR (f5 ILIKE "\t")) OR (hasAll(f1, .5)) +f3 = +f4 IN () +hasAny(f5, "%wild%") +f1 <= 1 +f6 > 'O''Reilly' +AND f3 = "a_b%" +(f5 IN ("a_b%", "hello world", "hello world", 0, 0)) OR (hasAny(f5, -.5)) +f6 = NULL +f1 IN (f6, 0.0000000001, -1e-10, "%wild%", "abc") +has(f1, 1e10) +f2 IS NULL +f3 IN (-1e-10, -3.5e-2, 6.02e23) +(f5 == f2) AND (hasAll(f6, f5)) +f3 IN [1 2 3] +f4 LIKE +hasAny(f2, 999999999999) +(f5 = -3.5e-2 +f6 = NULL +(f5 BETWEEN TRUE AND 1) OR (f6 >= 'O''Reilly') +f3 != 'test' +f5 >= 3.14 +f1 = NULL +f4 != "\t" +f5 IN (f5, f6, "abc") +f6 = 6.02e23 +f6 IN (' ', 1e10, f6) +f6 NOT LIKE ' ' +AND f6 = f4 +((f4 BETWEEN 0 AND f1) OR (f5 REGEXP 'O''Reilly')) OR (hasAny(f3, 3.14)) \ No newline at end of file From b9eecacab7a164f9b0798f05a9f1454ea8fd42e4 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Tue, 31 Mar 2026 14:16:27 +0530 Subject: [PATCH 59/78] chore: remove v1 metrics explorer code (#10764) * chore: remove v1 metrics explorer code * chore: fix ci * chore: fix tests * chore: address review comments --- docs/api/openapi.yml | 139 ++ ee/query-service/app/server.go | 1 - .../api/generated/services/metrics/index.ts | 173 +++ .../api/generated/services/sigNoz.schemas.ts | 49 + .../getInspectMetricsDetails.ts | 54 - .../api/metricsExplorer/getMetricDetails.ts | 75 - .../src/api/metricsExplorer/getMetricsList.ts | 67 - .../getMetricsListFilterKeys.ts | 44 - .../getMetricsListFilterValues.ts | 43 - .../api/metricsExplorer/getRelatedMetrics.ts | 60 - frontend/src/container/Home/Home.tsx | 44 +- .../container/MeterExplorer/Explorer/types.ts | 37 - .../Explorer/Explorer.styles.scss | 100 -- .../MetricsExplorer/Explorer/Explorer.tsx | 57 +- .../Explorer/RelatedMetrics.tsx | 153 -- .../Explorer/RelatedMetricsCard.tsx | 47 - .../MetricsExplorer/Explorer/types.ts | 34 +- .../Explorer/useGetRelatedMetricsGraphs.ts | 113 -- .../MetricsExplorer/Inspect/ExpandedView.tsx | 5 +- .../MetricsExplorer/Inspect/Inspect.tsx | 33 +- .../MetricsExplorer/Inspect/MetricFilters.tsx | 2 +- .../MetricsExplorer/Inspect/TableView.tsx | 2 +- .../Inspect/__tests__/ExpandedView.test.tsx | 8 +- .../Inspect/__tests__/GraphPopover.test.tsx | 3 +- .../Inspect/__tests__/GraphView.test.tsx | 12 +- .../Inspect/__tests__/Inspect.test.tsx | 140 +- .../Inspect/__tests__/QueryBuilder.test.tsx | 10 +- .../Inspect/__tests__/TableView.test.tsx | 19 +- .../MetricsExplorer/Inspect/constants.ts | 36 +- .../MetricsExplorer/Inspect/types.ts | 27 +- .../Inspect/useInspectMetrics.ts | 69 +- .../MetricsExplorer/Inspect/utils.ts | 2 +- .../MetricDetails/__tests__/Metadata.test.tsx | 6 +- .../Summary/MetricNameSearch.tsx | 37 +- .../Summary/__tests__/MetricsTable.test.tsx | 38 +- .../MetricsExplorer/Summary/utils.tsx | 9 - .../ReduceToFilter/ReduceToFilter.test.tsx | 6 +- .../useGetInspectMetricsDetails.ts | 55 - .../metricsExplorer/useGetMetricDetails.ts | 46 - .../metricsExplorer/useGetMetricsList.ts | 47 - .../useGetMetricsListFilterKeys.ts | 48 - .../useGetMetricsListFilterValues.ts | 42 - .../metricsExplorer/useGetRelatedMetrics.ts | 48 - .../queryBuilder/useFetchKeysAndValues.ts | 77 +- .../metricsExplorer/v2/getMetricMetadata.ts | 15 - .../signozapiserver/metricsexplorer.go | 38 + .../implmetricsexplorer/handler.go | 35 + .../implmetricsexplorer/module.go | 154 ++- .../metricsexplorer/metricsexplorer.go | 4 + .../app/clickhouseReader/reader.go | 1226 ----------------- pkg/query-service/app/http_handler.go | 31 - .../app/metricsexplorer/parser.go | 154 --- .../app/metricsexplorer/summary.go | 576 -------- pkg/query-service/app/server.go | 1 - pkg/query-service/app/summary.go | 214 --- pkg/query-service/interfaces/interface.go | 24 - .../model/metrics_explorer/summary.go | 173 --- pkg/query-service/utils/filter_conditions.go | 94 -- .../metricsexplorertypes/metricsexplorer.go | 36 + 59 files changed, 871 insertions(+), 4021 deletions(-) delete mode 100644 frontend/src/api/metricsExplorer/getInspectMetricsDetails.ts delete mode 100644 frontend/src/api/metricsExplorer/getMetricDetails.ts delete mode 100644 frontend/src/api/metricsExplorer/getMetricsList.ts delete mode 100644 frontend/src/api/metricsExplorer/getMetricsListFilterKeys.ts delete mode 100644 frontend/src/api/metricsExplorer/getMetricsListFilterValues.ts delete mode 100644 frontend/src/api/metricsExplorer/getRelatedMetrics.ts delete mode 100644 frontend/src/container/MeterExplorer/Explorer/types.ts delete mode 100644 frontend/src/container/MetricsExplorer/Explorer/RelatedMetrics.tsx delete mode 100644 frontend/src/container/MetricsExplorer/Explorer/RelatedMetricsCard.tsx delete mode 100644 frontend/src/container/MetricsExplorer/Explorer/useGetRelatedMetricsGraphs.ts delete mode 100644 frontend/src/hooks/metricsExplorer/useGetInspectMetricsDetails.ts delete mode 100644 frontend/src/hooks/metricsExplorer/useGetMetricDetails.ts delete mode 100644 frontend/src/hooks/metricsExplorer/useGetMetricsList.ts delete mode 100644 frontend/src/hooks/metricsExplorer/useGetMetricsListFilterKeys.ts delete mode 100644 frontend/src/hooks/metricsExplorer/useGetMetricsListFilterValues.ts delete mode 100644 frontend/src/hooks/metricsExplorer/useGetRelatedMetrics.ts delete mode 100644 frontend/src/types/api/metricsExplorer/v2/getMetricMetadata.ts delete mode 100644 pkg/query-service/app/metricsexplorer/parser.go delete mode 100644 pkg/query-service/app/metricsexplorer/summary.go delete mode 100644 pkg/query-service/app/summary.go delete mode 100644 pkg/query-service/model/metrics_explorer/summary.go diff --git a/docs/api/openapi.yml b/docs/api/openapi.yml index d069c995478..b092e45a790 100644 --- a/docs/api/openapi.yml +++ b/docs/api/openapi.yml @@ -1114,6 +1114,33 @@ components: enabled: type: boolean type: object + MetricsexplorertypesInspectMetricsRequest: + properties: + end: + format: int64 + type: integer + filter: + $ref: '#/components/schemas/Querybuildertypesv5Filter' + metricName: + type: string + start: + format: int64 + type: integer + required: + - metricName + - start + - end + type: object + MetricsexplorertypesInspectMetricsResponse: + properties: + series: + items: + $ref: '#/components/schemas/Querybuildertypesv5TimeSeries' + nullable: true + type: array + required: + - series + type: object MetricsexplorertypesListMetric: properties: description: @@ -1262,6 +1289,13 @@ components: - temporality - isMonotonic type: object + MetricsexplorertypesMetricsOnboardingResponse: + properties: + hasMetrics: + type: boolean + required: + - hasMetrics + type: object MetricsexplorertypesStat: properties: description: @@ -7750,6 +7784,111 @@ paths: summary: Update metric metadata tags: - metrics + /api/v2/metrics/inspect: + post: + deprecated: false + description: Returns raw time series data points for a metric within a time + range (max 30 minutes). Each series includes labels and timestamp/value pairs. + operationId: InspectMetrics + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MetricsexplorertypesInspectMetricsRequest' + responses: + "200": + content: + application/json: + schema: + properties: + data: + $ref: '#/components/schemas/MetricsexplorertypesInspectMetricsResponse' + status: + type: string + required: + - status + - data + type: object + description: OK + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Bad Request + "401": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Unauthorized + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Forbidden + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Internal Server Error + security: + - api_key: + - VIEWER + - tokenizer: + - VIEWER + summary: Inspect raw metric data points + tags: + - metrics + /api/v2/metrics/onboarding: + get: + deprecated: false + description: Lightweight endpoint that checks if any non-SigNoz metrics have + been ingested, used for onboarding status detection + operationId: GetMetricsOnboardingStatus + responses: + "200": + content: + application/json: + schema: + properties: + data: + $ref: '#/components/schemas/MetricsexplorertypesMetricsOnboardingResponse' + status: + type: string + required: + - status + - data + type: object + description: OK + "401": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Unauthorized + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Forbidden + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Internal Server Error + security: + - api_key: + - VIEWER + - tokenizer: + - VIEWER + summary: Check if non-SigNoz metrics have been received + tags: + - metrics /api/v2/metrics/stats: post: deprecated: false diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index 5aa530bdcc7..79ab2999e52 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -242,7 +242,6 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h apiHandler.RegisterWebSocketPaths(r, am) apiHandler.RegisterMessagingQueuesRoutes(r, am) apiHandler.RegisterThirdPartyApiRoutes(r, am) - apiHandler.MetricExplorerRoutes(r, am) apiHandler.RegisterTraceFunnelsRoutes(r, am) err := s.signoz.APIServer.AddToRouter(r) diff --git a/frontend/src/api/generated/services/metrics/index.ts b/frontend/src/api/generated/services/metrics/index.ts index 7f6703b2f41..20a06783cf0 100644 --- a/frontend/src/api/generated/services/metrics/index.ts +++ b/frontend/src/api/generated/services/metrics/index.ts @@ -31,10 +31,13 @@ import type { GetMetricHighlightsPathParameters, GetMetricMetadata200, GetMetricMetadataPathParameters, + GetMetricsOnboardingStatus200, GetMetricsStats200, GetMetricsTreemap200, + InspectMetrics200, ListMetrics200, ListMetricsParams, + MetricsexplorertypesInspectMetricsRequestDTO, MetricsexplorertypesStatsRequestDTO, MetricsexplorertypesTreemapRequestDTO, MetricsexplorertypesUpdateMetricMetadataRequestDTO, @@ -778,6 +781,176 @@ export const useUpdateMetricMetadata = < return useMutation(mutationOptions); }; +/** + * Returns raw time series data points for a metric within a time range (max 30 minutes). Each series includes labels and timestamp/value pairs. + * @summary Inspect raw metric data points + */ +export const inspectMetrics = ( + metricsexplorertypesInspectMetricsRequestDTO: BodyType, + signal?: AbortSignal, +) => { + return GeneratedAPIInstance({ + url: `/api/v2/metrics/inspect`, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + data: metricsexplorertypesInspectMetricsRequestDTO, + signal, + }); +}; + +export const getInspectMetricsMutationOptions = < + TError = ErrorType, + TContext = unknown +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; +}): UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + const mutationKey = ['inspectMetrics']; + const { mutation: mutationOptions } = options + ? options.mutation && + 'mutationKey' in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey } }; + + const mutationFn: MutationFunction< + Awaited>, + { data: BodyType } + > = (props) => { + const { data } = props ?? {}; + + return inspectMetrics(data); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type InspectMetricsMutationResult = NonNullable< + Awaited> +>; +export type InspectMetricsMutationBody = BodyType; +export type InspectMetricsMutationError = ErrorType; + +/** + * @summary Inspect raw metric data points + */ +export const useInspectMetrics = < + TError = ErrorType, + TContext = unknown +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; +}): UseMutationResult< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + const mutationOptions = getInspectMetricsMutationOptions(options); + + return useMutation(mutationOptions); +}; +/** + * Lightweight endpoint that checks if any non-SigNoz metrics have been ingested, used for onboarding status detection + * @summary Check if non-SigNoz metrics have been received + */ +export const getMetricsOnboardingStatus = (signal?: AbortSignal) => { + return GeneratedAPIInstance({ + url: `/api/v2/metrics/onboarding`, + method: 'GET', + signal, + }); +}; + +export const getGetMetricsOnboardingStatusQueryKey = () => { + return [`/api/v2/metrics/onboarding`] as const; +}; + +export const getGetMetricsOnboardingStatusQueryOptions = < + TData = Awaited>, + TError = ErrorType +>(options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; +}) => { + const { query: queryOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? getGetMetricsOnboardingStatusQueryKey(); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => getMetricsOnboardingStatus(signal); + + return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: QueryKey }; +}; + +export type GetMetricsOnboardingStatusQueryResult = NonNullable< + Awaited> +>; +export type GetMetricsOnboardingStatusQueryError = ErrorType; + +/** + * @summary Check if non-SigNoz metrics have been received + */ + +export function useGetMetricsOnboardingStatus< + TData = Awaited>, + TError = ErrorType +>(options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; +}): UseQueryResult & { queryKey: QueryKey } { + const queryOptions = getGetMetricsOnboardingStatusQueryOptions(options); + + const query = useQuery(queryOptions) as UseQueryResult & { + queryKey: QueryKey; + }; + + query.queryKey = queryOptions.queryKey; + + return query; +} + +/** + * @summary Check if non-SigNoz metrics have been received + */ +export const invalidateGetMetricsOnboardingStatus = async ( + queryClient: QueryClient, + options?: InvalidateOptions, +): Promise => { + await queryClient.invalidateQueries( + { queryKey: getGetMetricsOnboardingStatusQueryKey() }, + options, + ); + + return queryClient; +}; + /** * This endpoint provides list of metrics with their number of samples and timeseries for the given time range * @summary Get metrics statistics diff --git a/frontend/src/api/generated/services/sigNoz.schemas.ts b/frontend/src/api/generated/services/sigNoz.schemas.ts index b904cdc4962..2dc46a406e6 100644 --- a/frontend/src/api/generated/services/sigNoz.schemas.ts +++ b/frontend/src/api/generated/services/sigNoz.schemas.ts @@ -1363,6 +1363,32 @@ export interface GlobaltypesTokenizerConfigDTO { enabled?: boolean; } +export interface MetricsexplorertypesInspectMetricsRequestDTO { + /** + * @type integer + * @format int64 + */ + end: number; + filter?: Querybuildertypesv5FilterDTO; + /** + * @type string + */ + metricName: string; + /** + * @type integer + * @format int64 + */ + start: number; +} + +export interface MetricsexplorertypesInspectMetricsResponseDTO { + /** + * @type array + * @nullable true + */ + series: Querybuildertypesv5TimeSeriesDTO[] | null; +} + export interface MetricsexplorertypesListMetricDTO { /** * @type string @@ -1508,6 +1534,13 @@ export interface MetricsexplorertypesMetricMetadataDTO { unit: string; } +export interface MetricsexplorertypesMetricsOnboardingResponseDTO { + /** + * @type boolean + */ + hasMetrics: boolean; +} + export interface MetricsexplorertypesStatDTO { /** * @type string @@ -4391,6 +4424,22 @@ export type GetMetricMetadata200 = { export type UpdateMetricMetadataPathParameters = { metricName: string; }; +export type InspectMetrics200 = { + data: MetricsexplorertypesInspectMetricsResponseDTO; + /** + * @type string + */ + status: string; +}; + +export type GetMetricsOnboardingStatus200 = { + data: MetricsexplorertypesMetricsOnboardingResponseDTO; + /** + * @type string + */ + status: string; +}; + export type GetMetricsStats200 = { data: MetricsexplorertypesStatsResponseDTO; /** diff --git a/frontend/src/api/metricsExplorer/getInspectMetricsDetails.ts b/frontend/src/api/metricsExplorer/getInspectMetricsDetails.ts deleted file mode 100644 index cf6eadea741..00000000000 --- a/frontend/src/api/metricsExplorer/getInspectMetricsDetails.ts +++ /dev/null @@ -1,54 +0,0 @@ -import axios from 'api'; -import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; -import { AxiosError } from 'axios'; -import { ErrorResponse, SuccessResponse } from 'types/api'; -import { TagFilter } from 'types/api/queryBuilder/queryBuilderData'; - -export interface InspectMetricsRequest { - metricName: string; - start: number; - end: number; - filters: TagFilter; -} - -export interface InspectMetricsResponse { - status: string; - data: { - series: InspectMetricsSeries[]; - }; -} - -export interface InspectMetricsSeries { - title?: string; - strokeColor?: string; - labels: Record; - labelsArray: Array>; - values: InspectMetricsTimestampValue[]; -} - -interface InspectMetricsTimestampValue { - timestamp: number; - value: string; -} - -export const getInspectMetricsDetails = async ( - request: InspectMetricsRequest, - signal?: AbortSignal, - headers?: Record, -): Promise | ErrorResponse> => { - try { - const response = await axios.post(`/metrics/inspect`, request, { - signal, - headers, - }); - - return { - statusCode: 200, - error: null, - message: 'Success', - payload: response.data, - }; - } catch (error) { - return ErrorResponseHandler(error as AxiosError); - } -}; diff --git a/frontend/src/api/metricsExplorer/getMetricDetails.ts b/frontend/src/api/metricsExplorer/getMetricDetails.ts deleted file mode 100644 index 74bde3e6719..00000000000 --- a/frontend/src/api/metricsExplorer/getMetricDetails.ts +++ /dev/null @@ -1,75 +0,0 @@ -import axios from 'api'; -import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; -import { AxiosError } from 'axios'; -import { ErrorResponse, SuccessResponse } from 'types/api'; - -import { MetricType } from './getMetricsList'; - -export interface MetricDetails { - name: string; - description: string; - type: string; - unit: string; - timeseries: number; - samples: number; - timeSeriesTotal: number; - timeSeriesActive: number; - lastReceived: string; - attributes: MetricDetailsAttribute[] | null; - metadata?: { - metric_type: MetricType; - description: string; - unit: string; - temporality?: Temporality; - }; - alerts: MetricDetailsAlert[] | null; - dashboards: MetricDetailsDashboard[] | null; -} - -export enum Temporality { - CUMULATIVE = 'Cumulative', - DELTA = 'Delta', -} - -export interface MetricDetailsAttribute { - key: string; - value: string[]; - valueCount: number; -} - -export interface MetricDetailsAlert { - alert_name: string; - alert_id: string; -} - -export interface MetricDetailsDashboard { - dashboard_name: string; - dashboard_id: string; -} - -export interface MetricDetailsResponse { - status: string; - data: MetricDetails; -} - -export const getMetricDetails = async ( - metricName: string, - signal?: AbortSignal, - headers?: Record, -): Promise | ErrorResponse> => { - try { - const response = await axios.get(`/metrics/${metricName}/metadata`, { - signal, - headers, - }); - - return { - statusCode: 200, - error: null, - message: 'Success', - payload: response.data, - }; - } catch (error) { - return ErrorResponseHandler(error as AxiosError); - } -}; diff --git a/frontend/src/api/metricsExplorer/getMetricsList.ts b/frontend/src/api/metricsExplorer/getMetricsList.ts deleted file mode 100644 index 63fd5e4da4e..00000000000 --- a/frontend/src/api/metricsExplorer/getMetricsList.ts +++ /dev/null @@ -1,67 +0,0 @@ -import axios from 'api'; -import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; -import { AxiosError } from 'axios'; -import { - OrderByPayload, - TreemapViewType, -} from 'container/MetricsExplorer/Summary/types'; -import { ErrorResponse, SuccessResponse } from 'types/api'; -import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; -import { TagFilter } from 'types/api/queryBuilder/queryBuilderData'; - -export interface MetricsListPayload { - filters: TagFilter; - groupBy?: BaseAutocompleteData[]; - offset?: number; - limit?: number; - orderBy?: OrderByPayload; -} - -export enum MetricType { - SUM = 'Sum', - GAUGE = 'Gauge', - HISTOGRAM = 'Histogram', - SUMMARY = 'Summary', - EXPONENTIAL_HISTOGRAM = 'ExponentialHistogram', -} - -export interface MetricsListItemData { - metric_name: string; - description: string; - type: MetricType; - unit: string; - [TreemapViewType.TIMESERIES]: number; - [TreemapViewType.SAMPLES]: number; - lastReceived: string; -} - -export interface MetricsListResponse { - status: string; - data: { - metrics: MetricsListItemData[]; - total?: number; - }; -} - -export const getMetricsList = async ( - props: MetricsListPayload, - signal?: AbortSignal, - headers?: Record, -): Promise | ErrorResponse> => { - try { - const response = await axios.post('/metrics', props, { - signal, - headers, - }); - - return { - statusCode: 200, - error: null, - message: response.data.status, - payload: response.data, - params: props, - }; - } catch (error) { - return ErrorResponseHandler(error as AxiosError); - } -}; diff --git a/frontend/src/api/metricsExplorer/getMetricsListFilterKeys.ts b/frontend/src/api/metricsExplorer/getMetricsListFilterKeys.ts deleted file mode 100644 index 4fb15ab65ce..00000000000 --- a/frontend/src/api/metricsExplorer/getMetricsListFilterKeys.ts +++ /dev/null @@ -1,44 +0,0 @@ -import axios from 'api'; -import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; -import { AxiosError } from 'axios'; -import { ErrorResponse, SuccessResponse } from 'types/api'; -import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; - -export interface MetricsListFilterKeysResponse { - status: string; - data: { - metricColumns: string[]; - attributeKeys: BaseAutocompleteData[]; - }; -} - -export interface GetMetricsListFilterKeysParams { - searchText: string; - limit?: number; -} - -export const getMetricsListFilterKeys = async ( - params: GetMetricsListFilterKeysParams, - signal?: AbortSignal, - headers?: Record, -): Promise | ErrorResponse> => { - try { - const response = await axios.get('/metrics/filters/keys', { - params: { - searchText: params.searchText, - limit: params.limit, - }, - signal, - headers, - }); - - return { - statusCode: 200, - error: null, - message: response.data.status, - payload: response.data, - }; - } catch (error) { - return ErrorResponseHandler(error as AxiosError); - } -}; diff --git a/frontend/src/api/metricsExplorer/getMetricsListFilterValues.ts b/frontend/src/api/metricsExplorer/getMetricsListFilterValues.ts deleted file mode 100644 index 6b4cd6d3176..00000000000 --- a/frontend/src/api/metricsExplorer/getMetricsListFilterValues.ts +++ /dev/null @@ -1,43 +0,0 @@ -import axios from 'api'; -import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; -import { AxiosError } from 'axios'; -import { ErrorResponse, SuccessResponse } from 'types/api'; - -export interface MetricsListFilterValuesPayload { - filterAttributeKeyDataType: string; - filterKey: string; - searchText: string; - limit: number; -} - -export interface MetricsListFilterValuesResponse { - status: string; - data: { - filterValues: string[]; - }; -} - -export const getMetricsListFilterValues = async ( - props: MetricsListFilterValuesPayload, - signal?: AbortSignal, - headers?: Record, -): Promise< - SuccessResponse | ErrorResponse -> => { - try { - const response = await axios.post('/metrics/filters/values', props, { - signal, - headers, - }); - - return { - statusCode: 200, - error: null, - message: response.data.status, - payload: response.data, - params: props, - }; - } catch (error) { - return ErrorResponseHandler(error as AxiosError); - } -}; diff --git a/frontend/src/api/metricsExplorer/getRelatedMetrics.ts b/frontend/src/api/metricsExplorer/getRelatedMetrics.ts deleted file mode 100644 index 2f4aa5dfa6a..00000000000 --- a/frontend/src/api/metricsExplorer/getRelatedMetrics.ts +++ /dev/null @@ -1,60 +0,0 @@ -import axios from 'api'; -import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; -import { AxiosError } from 'axios'; -import { ErrorResponse, SuccessResponse } from 'types/api'; -import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; - -export interface RelatedMetricsPayload { - start: number; - end: number; - currentMetricName: string; -} - -export interface RelatedMetricDashboard { - dashboard_name: string; - dashboard_id: string; - widget_id: string; - widget_name: string; -} - -export interface RelatedMetricAlert { - alert_name: string; - alert_id: string; -} - -export interface RelatedMetric { - name: string; - query: IBuilderQuery; - dashboards: RelatedMetricDashboard[]; - alerts: RelatedMetricAlert[]; -} - -export interface RelatedMetricsResponse { - status: 'success'; - data: { - related_metrics: RelatedMetric[]; - }; -} - -export const getRelatedMetrics = async ( - props: RelatedMetricsPayload, - signal?: AbortSignal, - headers?: Record, -): Promise | ErrorResponse> => { - try { - const response = await axios.post('/metrics/related', props, { - signal, - headers, - }); - - return { - statusCode: 200, - error: null, - message: response.data.status, - payload: response.data, - params: props, - }; - } catch (error) { - return ErrorResponseHandler(error as AxiosError); - } -}; diff --git a/frontend/src/container/Home/Home.tsx b/frontend/src/container/Home/Home.tsx index a2b2867a67b..513cae2bb5e 100644 --- a/frontend/src/container/Home/Home.tsx +++ b/frontend/src/container/Home/Home.tsx @@ -1,10 +1,11 @@ /* eslint-disable sonarjs/no-duplicate-string */ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { useMutation, useQuery } from 'react-query'; import { Color } from '@signozhq/design-tokens'; import { Compass, Dot, House, Plus, Wrench } from '@signozhq/icons'; import { Button, Popover } from 'antd'; import logEvent from 'api/common/logEvent'; +import { useGetMetricsOnboardingStatus } from 'api/generated/services/metrics'; import listUserPreferences from 'api/v1/user/preferences/list'; import updateUserPreferenceAPI from 'api/v1/user/preferences/name/update'; import { PersistedAnnouncementBanner } from 'components/AnnouncementBanner'; @@ -15,10 +16,8 @@ import { ORG_PREFERENCES } from 'constants/orgPreferences'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import ROUTES from 'constants/routes'; -import { getMetricsListQuery } from 'container/MetricsExplorer/Summary/utils'; import { IS_SERVICE_ACCOUNTS_ENABLED } from 'container/ServiceAccountsSettings/config'; import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants'; -import { useGetMetricsList } from 'hooks/metricsExplorer/useGetMetricsList'; import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { useSafeNavigate } from 'hooks/useSafeNavigate'; @@ -127,38 +126,7 @@ export default function Home(): JSX.Element { ); // Detect Metrics - const query = useMemo(() => { - const baseQuery = getMetricsListQuery(); - - let queryStartTime = startTime; - let queryEndTime = endTime; - - if (!startTime || !endTime) { - const now = new Date(); - const startTime = new Date(now.getTime() - homeInterval); - const endTime = now; - - queryStartTime = startTime.getTime(); - queryEndTime = endTime.getTime(); - } - - return { - ...baseQuery, - limit: 10, - offset: 0, - filters: { - items: [], - op: 'AND', - }, - start: queryStartTime, - end: queryEndTime, - }; - }, [startTime, endTime]); - - const { data: metricsData } = useGetMetricsList(query, { - enabled: !!query, - queryKey: ['metricsList', query], - }); + const { data: metricsOnboardingData } = useGetMetricsOnboardingStatus(); const [isLogsIngestionActive, setIsLogsIngestionActive] = useState(false); const [isTracesIngestionActive, setIsTracesIngestionActive] = useState(false); @@ -284,14 +252,12 @@ export default function Home(): JSX.Element { }, [tracesData, handleUpdateChecklistDoneItem]); useEffect(() => { - const metricsDataTotal = metricsData?.payload?.data?.total ?? 0; - - if (metricsDataTotal > 0) { + if (metricsOnboardingData?.data?.hasMetrics) { setIsMetricsIngestionActive(true); handleUpdateChecklistDoneItem('ADD_DATA_SOURCE'); handleUpdateChecklistDoneItem('SEND_METRICS'); } - }, [metricsData, handleUpdateChecklistDoneItem]); + }, [metricsOnboardingData, handleUpdateChecklistDoneItem]); useEffect(() => { logEvent('Homepage: Visited', {}); diff --git a/frontend/src/container/MeterExplorer/Explorer/types.ts b/frontend/src/container/MeterExplorer/Explorer/types.ts deleted file mode 100644 index 2e618b8d706..00000000000 --- a/frontend/src/container/MeterExplorer/Explorer/types.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { UseQueryResult } from 'react-query'; -import { RelatedMetric } from 'api/metricsExplorer/getRelatedMetrics'; -import { SuccessResponse } from 'types/api'; -import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; - -export enum ExplorerTabs { - TIME_SERIES = 'time-series', - RELATED_METRICS = 'related-metrics', -} - -export interface TimeSeriesProps { - showOneChartPerQuery: boolean; -} - -export interface RelatedMetricsProps { - metricNames: string[]; -} - -export interface RelatedMetricsCardProps { - metric: RelatedMetricWithQueryResult; -} - -export interface UseGetRelatedMetricsGraphsProps { - selectedMetricName: string | null; - startMs: number; - endMs: number; -} - -export interface UseGetRelatedMetricsGraphsReturn { - relatedMetrics: RelatedMetricWithQueryResult[]; - isRelatedMetricsLoading: boolean; - isRelatedMetricsError: boolean; -} - -export interface RelatedMetricWithQueryResult extends RelatedMetric { - queryResult: UseQueryResult, unknown>; -} diff --git a/frontend/src/container/MetricsExplorer/Explorer/Explorer.styles.scss b/frontend/src/container/MetricsExplorer/Explorer/Explorer.styles.scss index 6b5661eb90d..54c475739be 100644 --- a/frontend/src/container/MetricsExplorer/Explorer/Explorer.styles.scss +++ b/frontend/src/container/MetricsExplorer/Explorer/Explorer.styles.scss @@ -30,31 +30,6 @@ } } - .explore-tabs { - margin: 15px 0; - .tab { - background-color: var(--bg-slate-500); - border-color: var(--bg-ink-200); - width: 180px; - padding: 16px 0; - display: flex; - justify-content: center; - align-items: center; - } - - .tab:first-of-type { - border-top-left-radius: 2px; - } - - .tab:last-of-type { - border-top-right-radius: 2px; - } - - .selected-view { - background: var(--bg-ink-500); - } - } - .explore-content { padding: 0 8px; @@ -116,81 +91,6 @@ width: 100%; height: fit-content; } - - .related-metrics-container { - width: 100%; - min-height: 300px; - display: flex; - flex-direction: column; - gap: 10px; - - .related-metrics-header { - display: flex; - align-items: center; - justify-content: flex-start; - - .metric-name-select { - width: 20%; - margin-right: 10px; - } - - .related-metrics-input { - width: 40%; - - .ant-input-wrapper { - .ant-input-group-addon { - .related-metrics-select { - width: 250px; - border: 1px solid var(--bg-slate-500) !important; - - .ant-select-selector { - text-align: left; - color: var(--text-vanilla-500) !important; - } - } - } - } - } - } - - .related-metrics-body { - margin-top: 20px; - max-height: 650px; - overflow-y: scroll; - - .related-metrics-card-container { - margin-bottom: 20px; - min-height: 640px; - - .related-metrics-card { - display: flex; - flex-direction: column; - gap: 16px; - - .related-metrics-card-error { - padding-top: 10px; - height: fit-content; - width: fit-content; - } - } - } - } - } - } -} - -.lightMode { - .metrics-explorer-explore-container { - .explore-tabs { - .tab { - background-color: var(--bg-vanilla-100); - border-color: var(--bg-vanilla-400); - } - - .selected-view { - background: var(--bg-vanilla-500); - } - } } } diff --git a/frontend/src/container/MetricsExplorer/Explorer/Explorer.tsx b/frontend/src/container/MetricsExplorer/Explorer/Explorer.tsx index ce42ae4763b..eae56983d73 100644 --- a/frontend/src/container/MetricsExplorer/Explorer/Explorer.tsx +++ b/frontend/src/container/MetricsExplorer/Explorer/Explorer.tsx @@ -32,7 +32,6 @@ import { v4 as uuid } from 'uuid'; import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events'; import MetricDetails from '../MetricDetails/MetricDetails'; import TimeSeries from './TimeSeries'; -import { ExplorerTabs } from './types'; import { getMetricUnits, splitQueryIntoOneChartPerQuery, @@ -95,7 +94,6 @@ function Explorer(): JSX.Element { const [disableOneChartPerQuery, toggleDisableOneChartPerQuery] = useState( false, ); - const [selectedTab] = useState(ExplorerTabs.TIME_SERIES); const [yAxisUnit, setYAxisUnit] = useState(); const unitsLength = useMemo(() => units.length, [units]); @@ -319,48 +317,21 @@ function Explorer(): JSX.Element { showFunctions={false} version="v3" /> - {/* TODO: Enable once we have resolved all related metrics issues */} - {/* - - - */}
- {selectedTab === ExplorerTabs.TIME_SERIES && ( - - )} - {/* TODO: Enable once we have resolved all related metrics issues */} - {/* {selectedTab === ExplorerTabs.RELATED_METRICS && ( - - )} */} +
(null); - const { maxTime, minTime } = useSelector( - (state) => state.globalTime, - ); - - const [selectedMetricName, setSelectedMetricName] = useState( - null, - ); - const [selectedRelatedMetric, setSelectedRelatedMetric] = useState('all'); - const [searchValue, setSearchValue] = useState(null); - - const startMs = useMemo(() => Math.floor(Number(minTime) / 1000000000), [ - minTime, - ]); - const endMs = useMemo(() => Math.floor(Number(maxTime) / 1000000000), [ - maxTime, - ]); - - useEffect(() => { - if (metricNames.length) { - setSelectedMetricName(metricNames[0]); - } - }, [metricNames]); - - const { - relatedMetrics, - isRelatedMetricsLoading, - isRelatedMetricsError, - } = useGetRelatedMetricsGraphs({ - selectedMetricName, - startMs, - endMs, - }); - - const metricNamesSelectOptions = useMemo( - () => - metricNames.map((name) => ({ - value: name, - label: name, - })), - [metricNames], - ); - - const relatedMetricsSelectOptions = useMemo(() => { - const options: { value: string; label: string }[] = [ - { - value: 'all', - label: 'All', - }, - ]; - relatedMetrics.forEach((metric) => { - options.push({ - value: metric.name, - label: metric.name, - }); - }); - return options; - }, [relatedMetrics]); - - const filteredRelatedMetrics = useMemo(() => { - let filteredMetrics: RelatedMetricWithQueryResult[] = []; - if (selectedRelatedMetric === 'all') { - filteredMetrics = [...relatedMetrics]; - } else { - filteredMetrics = relatedMetrics.filter( - (metric) => metric.name === selectedRelatedMetric, - ); - } - if (searchValue?.length) { - filteredMetrics = filteredMetrics.filter((metric) => - metric.name.toLowerCase().includes(searchValue?.toLowerCase() ?? ''), - ); - } - return filteredMetrics; - }, [relatedMetrics, selectedRelatedMetric, searchValue]); - - return ( -
-
- setSearchValue(e.target.value)} - bordered - addonBefore={ - - )} - /> - {errors.email && ( -

{errors.email.message}

- )} -
-

- Used only for notifications about this service account. It is not used for - authentication. -

- -
- - - value.length > 0 || 'At least one role is required', - }} - render={({ field }): JSX.Element => ( - - )} - /> - {errors.roles && ( -

{errors.roles.message}

- )} -
diff --git a/frontend/src/components/CreateServiceAccountModal/__tests__/CreateServiceAccountModal.test.tsx b/frontend/src/components/CreateServiceAccountModal/__tests__/CreateServiceAccountModal.test.tsx index 8dea4e2d3cf..a9d0ab94406 100644 --- a/frontend/src/components/CreateServiceAccountModal/__tests__/CreateServiceAccountModal.test.tsx +++ b/frontend/src/components/CreateServiceAccountModal/__tests__/CreateServiceAccountModal.test.tsx @@ -1,5 +1,4 @@ import { toast } from '@signozhq/sonner'; -import { listRolesSuccessResponse } from 'mocks-server/__mockdata__/roles'; import { rest, server } from 'mocks-server/server'; import { NuqsTestingAdapter } from 'nuqs/adapters/testing'; import { render, screen, userEvent, waitFor } from 'tests/test-utils'; @@ -12,7 +11,6 @@ jest.mock('@signozhq/sonner', () => ({ const mockToast = jest.mocked(toast); -const ROLES_ENDPOINT = '*/api/v1/roles'; const SERVICE_ACCOUNTS_ENDPOINT = '*/api/v1/service_accounts'; function renderModal(): ReturnType { @@ -27,9 +25,6 @@ describe('CreateServiceAccountModal', () => { beforeEach(() => { jest.clearAllMocks(); server.use( - rest.get(ROLES_ENDPOINT, (_, res, ctx) => - res(ctx.status(200), ctx.json(listRolesSuccessResponse)), - ), rest.post(SERVICE_ACCOUNTS_ENDPOINT, (_, res, ctx) => res(ctx.status(201), ctx.json({ status: 'success', data: {} })), ), @@ -48,38 +43,11 @@ describe('CreateServiceAccountModal', () => { ).toBeDisabled(); }); - it('submit button remains disabled when email is invalid', async () => { - const user = userEvent.setup({ pointerEventsCheck: 0 }); - renderModal(); - - await user.type(screen.getByPlaceholderText('Enter a name'), 'My Bot'); - await user.type( - screen.getByPlaceholderText('email@example.com'), - 'not-an-email', - ); - - await user.click(screen.getByText('Select roles')); - await user.click(await screen.findByTitle('signoz-admin')); - - await waitFor(() => - expect( - screen.getByRole('button', { name: /Create Service Account/i }), - ).toBeDisabled(), - ); - }); - it('successful submit shows toast.success and closes modal', async () => { const user = userEvent.setup({ pointerEventsCheck: 0 }); renderModal(); await user.type(screen.getByPlaceholderText('Enter a name'), 'Deploy Bot'); - await user.type( - screen.getByPlaceholderText('email@example.com'), - 'deploy@acme.io', - ); - - await user.click(screen.getByText('Select roles')); - await user.click(await screen.findByTitle('signoz-admin')); const submitBtn = screen.getByRole('button', { name: /Create Service Account/i, @@ -116,13 +84,6 @@ describe('CreateServiceAccountModal', () => { renderModal(); await user.type(screen.getByPlaceholderText('Enter a name'), 'Dupe Bot'); - await user.type( - screen.getByPlaceholderText('email@example.com'), - 'dupe@acme.io', - ); - - await user.click(screen.getByText('Select roles')); - await user.click(await screen.findByTitle('signoz-admin')); const submitBtn = screen.getByRole('button', { name: /Create Service Account/i, @@ -164,16 +125,4 @@ describe('CreateServiceAccountModal', () => { await screen.findByText('Name is required'); }); - - it('shows "Please enter a valid email address" for a malformed email', async () => { - const user = userEvent.setup({ pointerEventsCheck: 0 }); - renderModal(); - - await user.type( - screen.getByPlaceholderText('email@example.com'), - 'not-an-email', - ); - - await screen.findByText('Please enter a valid email address'); - }); }); diff --git a/frontend/src/components/RolesSelect/RolesSelect.tsx b/frontend/src/components/RolesSelect/RolesSelect.tsx index b0f8c4c6e04..12d2ea3bc3b 100644 --- a/frontend/src/components/RolesSelect/RolesSelect.tsx +++ b/frontend/src/components/RolesSelect/RolesSelect.tsx @@ -34,7 +34,7 @@ export function useRoles(): { export function getRoleOptions(roles: AuthtypesRoleDTO[]): RoleOption[] { return roles.map((role) => ({ label: role.name ?? '', - value: role.name ?? '', + value: role.id ?? '', })); } diff --git a/frontend/src/components/ServiceAccountDrawer/DisableAccountModal.tsx b/frontend/src/components/ServiceAccountDrawer/DeleteAccountModal.tsx similarity index 66% rename from frontend/src/components/ServiceAccountDrawer/DisableAccountModal.tsx rename to frontend/src/components/ServiceAccountDrawer/DeleteAccountModal.tsx index 32deed10682..6eba2a2d6d7 100644 --- a/frontend/src/components/ServiceAccountDrawer/DisableAccountModal.tsx +++ b/frontend/src/components/ServiceAccountDrawer/DeleteAccountModal.tsx @@ -1,13 +1,13 @@ import { useQueryClient } from 'react-query'; import { Button } from '@signozhq/button'; import { DialogFooter, DialogWrapper } from '@signozhq/dialog'; -import { PowerOff, X } from '@signozhq/icons'; +import { Trash2, X } from '@signozhq/icons'; import { toast } from '@signozhq/sonner'; import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs'; import { getGetServiceAccountQueryKey, invalidateListServiceAccounts, - useUpdateServiceAccountStatus, + useDeleteServiceAccount, } from 'api/generated/services/serviceaccount'; import type { RenderErrorResponseDTO, @@ -17,14 +17,14 @@ import { AxiosError } from 'axios'; import { SA_QUERY_PARAMS } from 'container/ServiceAccountsSettings/constants'; import { parseAsBoolean, useQueryState } from 'nuqs'; -function DisableAccountModal(): JSX.Element { +function DeleteAccountModal(): JSX.Element { const queryClient = useQueryClient(); const [accountId, setAccountId] = useQueryState(SA_QUERY_PARAMS.ACCOUNT); - const [isDisableOpen, setIsDisableOpen] = useQueryState( - SA_QUERY_PARAMS.DISABLE_SA, + const [isDeleteOpen, setIsDeleteOpen] = useQueryState( + SA_QUERY_PARAMS.DELETE_SA, parseAsBoolean.withDefault(false), ); - const open = !!isDisableOpen && !!accountId; + const open = !!isDeleteOpen && !!accountId; const cachedAccount = accountId ? queryClient.getQueryData<{ @@ -34,13 +34,13 @@ function DisableAccountModal(): JSX.Element { const accountName = cachedAccount?.data?.name; const { - mutate: updateStatus, - isLoading: isDisabling, - } = useUpdateServiceAccountStatus({ + mutate: deleteAccount, + isLoading: isDeleting, + } = useDeleteServiceAccount({ mutation: { onSuccess: async () => { - toast.success('Service account disabled', { richColors: true }); - await setIsDisableOpen(null); + toast.success('Service account deleted', { richColors: true }); + await setIsDeleteOpen(null); await setAccountId(null); await invalidateListServiceAccounts(queryClient); }, @@ -48,7 +48,7 @@ function DisableAccountModal(): JSX.Element { const errMessage = convertToApiError( error as AxiosError | null, - )?.getErrorMessage() || 'Failed to disable service account'; + )?.getErrorMessage() || 'Failed to delete service account'; toast.error(errMessage, { richColors: true }); }, }, @@ -58,14 +58,13 @@ function DisableAccountModal(): JSX.Element { if (!accountId) { return; } - updateStatus({ + deleteAccount({ pathParams: { id: accountId }, - data: { status: 'DISABLED' }, }); } function handleCancel(): void { - setIsDisableOpen(null); + setIsDeleteOpen(null); } return ( @@ -76,17 +75,18 @@ function DisableAccountModal(): JSX.Element { handleCancel(); } }} - title={`Disable service account ${accountName ?? ''}?`} + title={`Delete service account ${accountName ?? ''}?`} width="narrow" - className="alert-dialog sa-disable-dialog" + className="alert-dialog sa-delete-dialog" showCloseButton={false} disableOutsideClick={false} > -

- Disabling this service account will revoke access for all its keys. Any - systems using this account will lose access immediately. +

+ Are you sure you want to delete {accountName}? This action + cannot be undone. All keys associated with this service account will be + permanently removed.

- + ); } -export default DisableAccountModal; +export default DeleteAccountModal; diff --git a/frontend/src/components/ServiceAccountDrawer/EditKeyModal/EditKeyForm.tsx b/frontend/src/components/ServiceAccountDrawer/EditKeyModal/EditKeyForm.tsx index a05277b8de9..55510a36a13 100644 --- a/frontend/src/components/ServiceAccountDrawer/EditKeyModal/EditKeyForm.tsx +++ b/frontend/src/components/ServiceAccountDrawer/EditKeyModal/EditKeyForm.tsx @@ -6,7 +6,7 @@ import { LockKeyhole, Trash2, X } from '@signozhq/icons'; import { Input } from '@signozhq/input'; import { ToggleGroup, ToggleGroupItem } from '@signozhq/toggle-group'; import { DatePicker } from 'antd'; -import type { ServiceaccounttypesFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas'; +import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas'; import { popupContainer } from 'utils/selectPopupContainer'; import { disabledDate, formatLastObservedAt } from '../utils'; @@ -17,7 +17,7 @@ export interface EditKeyFormProps { register: UseFormRegister; control: Control; expiryMode: ExpiryMode; - keyItem: ServiceaccounttypesFactorAPIKeyDTO | null; + keyItem: ServiceaccounttypesGettableFactorAPIKeyDTO | null; isSaving: boolean; isDirty: boolean; onSubmit: () => void; diff --git a/frontend/src/components/ServiceAccountDrawer/EditKeyModal/index.tsx b/frontend/src/components/ServiceAccountDrawer/EditKeyModal/index.tsx index e03c6fb4219..c2d0c1b2eb0 100644 --- a/frontend/src/components/ServiceAccountDrawer/EditKeyModal/index.tsx +++ b/frontend/src/components/ServiceAccountDrawer/EditKeyModal/index.tsx @@ -11,7 +11,7 @@ import { } from 'api/generated/services/serviceaccount'; import type { RenderErrorResponseDTO, - ServiceaccounttypesFactorAPIKeyDTO, + ServiceaccounttypesGettableFactorAPIKeyDTO, } from 'api/generated/services/sigNoz.schemas'; import { AxiosError } from 'axios'; import { SA_QUERY_PARAMS } from 'container/ServiceAccountsSettings/constants'; @@ -27,7 +27,7 @@ import { DEFAULT_FORM_VALUES, ExpiryMode } from './types'; import './EditKeyModal.styles.scss'; export interface EditKeyModalProps { - keyItem: ServiceaccounttypesFactorAPIKeyDTO | null; + keyItem: ServiceaccounttypesGettableFactorAPIKeyDTO | null; } function EditKeyModal({ keyItem }: EditKeyModalProps): JSX.Element { diff --git a/frontend/src/components/ServiceAccountDrawer/KeysTab.tsx b/frontend/src/components/ServiceAccountDrawer/KeysTab.tsx index f33966f72b6..1aacbc52ddd 100644 --- a/frontend/src/components/ServiceAccountDrawer/KeysTab.tsx +++ b/frontend/src/components/ServiceAccountDrawer/KeysTab.tsx @@ -3,7 +3,7 @@ import { Button } from '@signozhq/button'; import { KeyRound, X } from '@signozhq/icons'; import { Skeleton, Table, Tooltip } from 'antd'; import type { ColumnsType } from 'antd/es/table/interface'; -import type { ServiceaccounttypesFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas'; +import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas'; import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats'; import dayjs from 'dayjs'; import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs'; @@ -14,7 +14,7 @@ import RevokeKeyModal from './RevokeKeyModal'; import { formatLastObservedAt } from './utils'; interface KeysTabProps { - keys: ServiceaccounttypesFactorAPIKeyDTO[]; + keys: ServiceaccounttypesGettableFactorAPIKeyDTO[]; isLoading: boolean; isDisabled?: boolean; currentPage: number; @@ -44,7 +44,7 @@ function buildColumns({ isDisabled, onRevokeClick, handleformatLastObservedAt, -}: BuildColumnsParams): ColumnsType { +}: BuildColumnsParams): ColumnsType { return [ { title: 'Name', @@ -183,7 +183,7 @@ function KeysTab({ return ( <> {/* Todo: use new table component from periscope when ready */} - + columns={columns} dataSource={keys} rowKey="id" diff --git a/frontend/src/components/ServiceAccountDrawer/OverviewTab.tsx b/frontend/src/components/ServiceAccountDrawer/OverviewTab.tsx index 98b3e968f51..1fc7df75c00 100644 --- a/frontend/src/components/ServiceAccountDrawer/OverviewTab.tsx +++ b/frontend/src/components/ServiceAccountDrawer/OverviewTab.tsx @@ -9,6 +9,9 @@ import { ServiceAccountRow } from 'container/ServiceAccountsSettings/utils'; import { useTimezone } from 'providers/Timezone'; import APIError from 'types/api/error'; +import SaveErrorItem from './SaveErrorItem'; +import type { SaveError } from './utils'; + interface OverviewTabProps { account: ServiceAccountRow; localName: string; @@ -21,6 +24,7 @@ interface OverviewTabProps { rolesError?: boolean; rolesErrorObj?: APIError | undefined; onRefetchRoles?: () => void; + saveErrors?: SaveError[]; } function OverviewTab({ @@ -35,6 +39,7 @@ function OverviewTab({ rolesError, rolesErrorObj, onRefetchRoles, + saveErrors = [], }: OverviewTabProps): JSX.Element { const { formatTimezoneAdjustedTimestamp } = useTimezone(); @@ -92,11 +97,14 @@ function OverviewTab({
{localRoles.length > 0 ? ( - localRoles.map((r) => ( - - {r} - - )) + localRoles.map((roleId) => { + const role = availableRoles.find((r) => r.id === roleId); + return ( + + {role?.name ?? roleId} + + ); + }) ) : ( )} @@ -126,9 +134,13 @@ function OverviewTab({ ACTIVE + ) : account.status?.toUpperCase() === 'DELETED' ? ( + + DELETED + ) : ( - DISABLED + {account.status ? account.status.toUpperCase() : 'UNKNOWN'} )}
@@ -143,6 +155,19 @@ function OverviewTab({ {formatTimestamp(account.updatedAt)}
+ + {saveErrors.length > 0 && ( +
+ {saveErrors.map(({ context, apiError, onRetry }) => ( + + ))} +
+ )} ); } diff --git a/frontend/src/components/ServiceAccountDrawer/RevokeKeyModal.tsx b/frontend/src/components/ServiceAccountDrawer/RevokeKeyModal.tsx index 40ad15bacab..690a020d6e6 100644 --- a/frontend/src/components/ServiceAccountDrawer/RevokeKeyModal.tsx +++ b/frontend/src/components/ServiceAccountDrawer/RevokeKeyModal.tsx @@ -11,7 +11,7 @@ import { } from 'api/generated/services/serviceaccount'; import type { RenderErrorResponseDTO, - ServiceaccounttypesFactorAPIKeyDTO, + ServiceaccounttypesGettableFactorAPIKeyDTO, } from 'api/generated/services/sigNoz.schemas'; import { AxiosError } from 'axios'; import { SA_QUERY_PARAMS } from 'container/ServiceAccountsSettings/constants'; @@ -64,9 +64,9 @@ function RevokeKeyModal(): JSX.Element { const open = !!revokeKeyId && !!accountId; const cachedKeys = accountId - ? queryClient.getQueryData<{ data: ServiceaccounttypesFactorAPIKeyDTO[] }>( - getListServiceAccountKeysQueryKey({ id: accountId }), - ) + ? queryClient.getQueryData<{ + data: ServiceaccounttypesGettableFactorAPIKeyDTO[]; + }>(getListServiceAccountKeysQueryKey({ id: accountId })) : null; const keyName = cachedKeys?.data?.find((k) => k.id === revokeKeyId)?.name; diff --git a/frontend/src/components/ServiceAccountDrawer/SaveErrorItem.tsx b/frontend/src/components/ServiceAccountDrawer/SaveErrorItem.tsx new file mode 100644 index 00000000000..bb7eb191753 --- /dev/null +++ b/frontend/src/components/ServiceAccountDrawer/SaveErrorItem.tsx @@ -0,0 +1,74 @@ +import { useState } from 'react'; +import { Button } from '@signozhq/button'; +import { Color } from '@signozhq/design-tokens'; +import { ChevronDown, ChevronUp, CircleAlert, RotateCw } from '@signozhq/icons'; +import ErrorContent from 'components/ErrorModal/components/ErrorContent'; +import APIError from 'types/api/error'; + +interface SaveErrorItemProps { + context: string; + apiError: APIError; + onRetry?: () => void | Promise; +} + +function SaveErrorItem({ + context, + apiError, + onRetry, +}: SaveErrorItemProps): JSX.Element { + const [expanded, setExpanded] = useState(false); + const [isRetrying, setIsRetrying] = useState(false); + + const ChevronIcon = expanded ? ChevronUp : ChevronDown; + + return ( +
+
{ + if (!isRetrying) { + setExpanded((prev) => !prev); + } + }} + > + + + {isRetrying ? 'Retrying...' : `${context}: ${apiError.getErrorMessage()}`} + + {onRetry && !isRetrying && ( + + )} + {!isRetrying && ( + + )} +
+ + {expanded && !isRetrying && ( +
+ +
+ )} +
+ ); +} + +export default SaveErrorItem; diff --git a/frontend/src/components/ServiceAccountDrawer/ServiceAccountDrawer.styles.scss b/frontend/src/components/ServiceAccountDrawer/ServiceAccountDrawer.styles.scss index a6804dffee0..9d12ff07470 100644 --- a/frontend/src/components/ServiceAccountDrawer/ServiceAccountDrawer.styles.scss +++ b/frontend/src/components/ServiceAccountDrawer/ServiceAccountDrawer.styles.scss @@ -92,6 +92,23 @@ display: flex; flex-direction: column; gap: var(--spacing-8); + + &::-webkit-scrollbar { + width: 0.25rem; + } + + &::-webkit-scrollbar-thumb { + background: rgba(136, 136, 136, 0.4); + border-radius: 0.125rem; + + &:hover { + background: rgba(136, 136, 136, 0.7); + } + } + + &::-webkit-scrollbar-track { + background: transparent; + } } &__footer { @@ -239,6 +256,113 @@ letter-spacing: 0.48px; text-transform: uppercase; } + + &__save-errors { + display: flex; + flex-direction: column; + gap: var(--spacing-2); + } +} + +.sa-error-item { + border: 1px solid var(--l1-border); + border-radius: 4px; + overflow: hidden; + + &__header { + display: flex; + align-items: center; + gap: var(--spacing-3); + width: 100%; + padding: var(--padding-2) var(--padding-4); + background: transparent; + border: none; + cursor: pointer; + text-align: left; + outline: none; + + &:hover { + background: rgba(229, 72, 77, 0.08); + } + + &:focus-visible { + outline: 2px solid var(--primary); + outline-offset: -2px; + } + + &[aria-disabled='true'] { + cursor: default; + pointer-events: none; + } + } + + &:hover { + border-color: var(--callout-error-border); + } + + &__icon { + flex-shrink: 0; + color: var(--bg-cherry-500); + } + + &__title { + flex: 1; + min-width: 0; + font-size: var(--font-size-xs); + font-weight: var(--font-weight-medium); + color: var(--bg-cherry-500); + line-height: var(--line-height-18); + letter-spacing: -0.06px; + text-align: left; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &__chevron { + flex-shrink: 0; + color: var(--l2-foreground); + } + + &__body { + border-top: 1px solid var(--l1-border); + + .error-content { + &__summary { + padding: 10px 12px; + } + + &__summary-left { + gap: 6px; + } + + &__error-code { + font-size: 12px; + line-height: 18px; + } + + &__error-message { + font-size: 11px; + line-height: 16px; + } + + &__docs-button { + font-size: 11px; + padding: 5px 8px; + } + + &__message-badge { + padding: 0 12px 10px; + gap: 8px; + } + + &__message-item { + font-size: 11px; + padding: 2px 12px 2px 22px; + margin-bottom: 2px; + } + } + } } .keys-tab { @@ -429,7 +553,7 @@ } } -.sa-disable-dialog { +.sa-delete-dialog { background: var(--l2-background); border: 1px solid var(--l2-border); diff --git a/frontend/src/components/ServiceAccountDrawer/ServiceAccountDrawer.tsx b/frontend/src/components/ServiceAccountDrawer/ServiceAccountDrawer.tsx index a801e4df54c..afad688d427 100644 --- a/frontend/src/components/ServiceAccountDrawer/ServiceAccountDrawer.tsx +++ b/frontend/src/components/ServiceAccountDrawer/ServiceAccountDrawer.tsx @@ -1,25 +1,29 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useQueryClient } from 'react-query'; import { Button } from '@signozhq/button'; import { DrawerWrapper } from '@signozhq/drawer'; -import { Key, LayoutGrid, Plus, PowerOff, X } from '@signozhq/icons'; +import { Key, LayoutGrid, Plus, Trash2, X } from '@signozhq/icons'; import { toast } from '@signozhq/sonner'; import { ToggleGroup, ToggleGroupItem } from '@signozhq/toggle-group'; import { Pagination, Skeleton } from 'antd'; import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs'; import { + getListServiceAccountsQueryKey, useGetServiceAccount, useListServiceAccountKeys, useUpdateServiceAccount, } from 'api/generated/services/serviceaccount'; -import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas'; +import type { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas'; import { AxiosError } from 'axios'; import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace'; import { useRoles } from 'components/RolesSelect'; import { SA_QUERY_PARAMS } from 'container/ServiceAccountsSettings/constants'; import { ServiceAccountRow, + ServiceAccountStatus, toServiceAccountRow, } from 'container/ServiceAccountsSettings/utils'; +import { useServiceAccountRoleManager } from 'hooks/serviceAccount/useServiceAccountRoleManager'; import { parseAsBoolean, parseAsInteger, @@ -27,12 +31,14 @@ import { parseAsStringEnum, useQueryState, } from 'nuqs'; +import APIError from 'types/api/error'; import { toAPIError } from 'utils/errorUtils'; import AddKeyModal from './AddKeyModal'; -import DisableAccountModal from './DisableAccountModal'; +import DeleteAccountModal from './DeleteAccountModal'; import KeysTab from './KeysTab'; import OverviewTab from './OverviewTab'; +import type { SaveError } from './utils'; import { ServiceAccountDrawerTab } from './utils'; import './ServiceAccountDrawer.styles.scss'; @@ -69,12 +75,16 @@ function ServiceAccountDrawer({ SA_QUERY_PARAMS.ADD_KEY, parseAsBoolean.withDefault(false), ); - const [, setIsDisableOpen] = useQueryState( - SA_QUERY_PARAMS.DISABLE_SA, + const [, setIsDeleteOpen] = useQueryState( + SA_QUERY_PARAMS.DELETE_SA, parseAsBoolean.withDefault(false), ); const [localName, setLocalName] = useState(''); const [localRoles, setLocalRoles] = useState([]); + const [isSaving, setIsSaving] = useState(false); + const [saveErrors, setSaveErrors] = useState([]); + + const queryClient = useQueryClient(); const { data: accountData, @@ -93,21 +103,30 @@ function ServiceAccountDrawer({ [accountData], ); + const { currentRoles, applyDiff } = useServiceAccountRoleManager( + selectedAccountId ?? '', + ); + useEffect(() => { - if (account) { - setLocalName(account.name ?? ''); - setLocalRoles(account.roles ?? []); + if (account?.id) { + setLocalName(account?.name ?? ''); setKeysPage(1); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [account?.id]); + setSaveErrors([]); + }, [account?.id, account?.name, setKeysPage]); + + useEffect(() => { + setLocalRoles(currentRoles.map((r) => r.id).filter(Boolean) as string[]); + }, [currentRoles]); - const isDisabled = account?.status?.toUpperCase() !== 'ACTIVE'; + const isDeleted = + account?.status?.toUpperCase() === ServiceAccountStatus.Deleted; const isDirty = account !== null && (localName !== (account.name ?? '') || - JSON.stringify(localRoles) !== JSON.stringify(account.roles ?? [])); + JSON.stringify([...localRoles].sort()) !== + JSON.stringify([...currentRoles.map((r) => r.id).filter(Boolean)].sort())); const { roles: availableRoles, @@ -133,51 +152,189 @@ function ServiceAccountDrawer({ } }, [keysLoading, keys.length, keysPage, setKeysPage]); - const { mutate: updateAccount, isLoading: isSaving } = useUpdateServiceAccount( - { - mutation: { - onSuccess: () => { - toast.success('Service account updated successfully', { - richColors: true, - }); - refetchAccount(); - onSuccess({ closeDrawer: false }); - }, - onError: (error) => { - const errMessage = - convertToApiError( - error as AxiosError | null, - )?.getErrorMessage() || 'Failed to update service account'; - toast.error(errMessage, { richColors: true }); - }, - }, + // the retry for this mutation is safe due to the api being idempotent on backend + const { mutateAsync: updateMutateAsync } = useUpdateServiceAccount(); + + const toSaveApiError = useCallback( + (err: unknown): APIError => + convertToApiError(err as AxiosError) ?? + toAPIError(err as AxiosError), + [], + ); + + const retryNameUpdate = useCallback(async (): Promise => { + if (!account) { + return; + } + try { + await updateMutateAsync({ + pathParams: { id: account.id }, + data: { name: localName }, + }); + setSaveErrors((prev) => prev.filter((e) => e.context !== 'Name update')); + refetchAccount(); + queryClient.invalidateQueries(getListServiceAccountsQueryKey()); + } catch (err) { + setSaveErrors((prev) => + prev.map((e) => + e.context === 'Name update' ? { ...e, apiError: toSaveApiError(err) } : e, + ), + ); + } + }, [ + account, + localName, + updateMutateAsync, + refetchAccount, + queryClient, + toSaveApiError, + ]); + + const handleNameChange = useCallback((name: string): void => { + setLocalName(name); + setSaveErrors((prev) => prev.filter((e) => e.context !== 'Name update')); + }, []); + + const makeRoleRetry = useCallback( + ( + context: string, + rawRetry: () => Promise, + ) => async (): Promise => { + try { + await rawRetry(); + setSaveErrors((prev) => prev.filter((e) => e.context !== context)); + } catch (err) { + setSaveErrors((prev) => + prev.map((e) => + e.context === context ? { ...e, apiError: toSaveApiError(err) } : e, + ), + ); + } }, + [toSaveApiError], ); - function handleSave(): void { + const retryRolesUpdate = useCallback(async (): Promise => { + try { + const failures = await applyDiff(localRoles, availableRoles); + if (failures.length === 0) { + setSaveErrors((prev) => prev.filter((e) => e.context !== 'Roles update')); + } else { + setSaveErrors((prev) => { + const rest = prev.filter((e) => e.context !== 'Roles update'); + const roleErrors = failures.map((f) => { + const ctx = `Role '${f.roleName}'`; + return { + context: ctx, + apiError: toSaveApiError(f.error), + onRetry: makeRoleRetry(ctx, f.onRetry), + }; + }); + return [...rest, ...roleErrors]; + }); + } + } catch (err) { + setSaveErrors((prev) => + prev.map((e) => + e.context === 'Roles update' ? { ...e, apiError: toSaveApiError(err) } : e, + ), + ); + } + }, [localRoles, availableRoles, applyDiff, toSaveApiError, makeRoleRetry]); + + const handleSave = useCallback(async (): Promise => { if (!account || !isDirty) { return; } - updateAccount({ - pathParams: { id: account.id }, - data: { name: localName, email: account.email, roles: localRoles }, - }); - } + setSaveErrors([]); + setIsSaving(true); + try { + const namePromise = + localName !== (account.name ?? '') + ? updateMutateAsync({ + pathParams: { id: account.id }, + data: { name: localName }, + }) + : Promise.resolve(); + + const [nameResult, rolesResult] = await Promise.allSettled([ + namePromise, + applyDiff(localRoles, availableRoles), + ]); + + const errors: SaveError[] = []; + + if (nameResult.status === 'rejected') { + errors.push({ + context: 'Name update', + apiError: toSaveApiError(nameResult.reason), + onRetry: retryNameUpdate, + }); + } + + if (rolesResult.status === 'rejected') { + errors.push({ + context: 'Roles update', + apiError: toSaveApiError(rolesResult.reason), + onRetry: retryRolesUpdate, + }); + } else { + for (const failure of rolesResult.value) { + const context = `Role '${failure.roleName}'`; + errors.push({ + context, + apiError: toSaveApiError(failure.error), + onRetry: makeRoleRetry(context, failure.onRetry), + }); + } + } + + if (errors.length > 0) { + setSaveErrors(errors); + } else { + toast.success('Service account updated successfully', { + richColors: true, + }); + onSuccess({ closeDrawer: false }); + } + + refetchAccount(); + queryClient.invalidateQueries(getListServiceAccountsQueryKey()); + } finally { + setIsSaving(false); + } + }, [ + account, + isDirty, + localName, + localRoles, + availableRoles, + updateMutateAsync, + applyDiff, + refetchAccount, + onSuccess, + queryClient, + toSaveApiError, + retryNameUpdate, + makeRoleRetry, + retryRolesUpdate, + ]); const handleClose = useCallback((): void => { - setIsDisableOpen(null); + setIsDeleteOpen(null); setIsAddKeyOpen(null); setSelectedAccountId(null); setActiveTab(null); setKeysPage(null); setEditKeyId(null); + setSaveErrors([]); }, [ setSelectedAccountId, setActiveTab, setKeysPage, setEditKeyId, setIsAddKeyOpen, - setIsDisableOpen, + setIsDeleteOpen, ]); const drawerContent = ( @@ -220,7 +377,7 @@ function ServiceAccountDrawer({ variant="outlined" size="sm" color="secondary" - disabled={isDisabled} + disabled={isDeleted} onClick={(): void => { setIsAddKeyOpen(true); }} @@ -251,22 +408,23 @@ function ServiceAccountDrawer({ )} {activeTab === ServiceAccountDrawerTab.Keys && ( @@ -298,20 +456,20 @@ function ServiceAccountDrawer({ /> ) : ( <> - {!isDisabled && ( + {!isDeleted && ( )} - {!isDisabled && ( + {!isDeleted && (
- - ), - children: ( -
- {APIKey?.createdByUser && ( - - Creator - - - {APIKey?.createdByUser?.displayName?.substring(0, 1)} - - - - {APIKey.createdByUser?.displayName} - - -
{APIKey.createdByUser?.email}
- -
- )} - - Created on - - {createdOn} - - - {updatedOn && ( - - Updated on - - {updatedOn} - - - )} - - - Expires on - - {expiresOn} - - -
- ), - }, - ]; - - return ( -
- - -
-
- - Last used - {formattedDateAndTime} -
- - {!isExpired && expiresIn <= EXPIRATION_WITHIN_SEVEN_DAYS && ( -
- Expires {dayjs().to(expiresOn)} -
- )} - - {isExpired && ( -
- Expired -
- )} -
-
- ); - }, - }, - ]; - - return ( -
-
-
- API Keys - - Create and manage API keys for the SigNoz API - -
- -
- } - value={searchValue} - onChange={handleSearch} - /> - - -
- - - `${range[0]}-${range[1]} of ${total} keys`, - }} - /> - - - {/* Delete Key Modal */} - Delete Key} - open={isDeleteModalOpen} - closable - afterClose={handleModalClose} - onCancel={hideDeleteViewModal} - destroyOnClose - footer={[ - , - , - ]} - > - - {t('delete_confirm_message', { - keyName: activeAPIKey?.name, - })} - - - - {/* Edit Key Modal */} - } - > - Cancel - , - , - ]} - > -
- - - - - - - - -
- Admin -
-
- -
- Editor -
-
- -
- Viewer -
-
-
-
-
- -
- - {/* Create New Key Modal */} - } - > - Copy key and close - , - ] - : [ - , - , - ] - } - > - {!showNewAPIKeyDetails && ( -
- - - - - - - - -
- Admin -
-
- -
- Editor -
-
- -
- Viewer -
-
-
-
-
- -
- - - -
- - {{ if .format.Header.Enabled }} - - - - {{ end }} - - - - {{ if .format.Footer.Enabled }} - - - - {{ end }} -
- SigNoz -
-

- Hi there, -

-

- An API key was {{.Event}} for your service account {{.Name}}. -

- - - - -
- - - - - - - - - - -
-

- Key ID: {{.KeyID}} -

-
-

- Key Name: {{.KeyName}} -

-
-

- Created At: {{.KeyCreatedAt}} -

-
-
- {{ if .format.Help.Enabled }} -

- Need help? Chat with our team in the SigNoz application or email us at {{.format.Help.Email}}. -

- {{ end }} -

- Thanks,
The SigNoz Team -

-
-

- Terms of Service - Privacy Policy -

-

- © 2026 SigNoz Inc. -

-
-
- - - \ No newline at end of file diff --git a/tests/integration/fixtures/serviceaccount.py b/tests/integration/fixtures/serviceaccount.py new file mode 100644 index 00000000000..9a21bd66916 --- /dev/null +++ b/tests/integration/fixtures/serviceaccount.py @@ -0,0 +1,102 @@ +"""Fixtures and helpers for service account tests.""" + +from http import HTTPStatus + +import requests + +from fixtures import types +from fixtures.logger import setup_logger + +logger = setup_logger(__name__) + +SERVICE_ACCOUNT_BASE = "/api/v1/service_accounts" +ROLES_BASE = "/api/v1/roles" + + +def find_role_by_name(signoz: types.SigNoz, token: str, name: str) -> str: + """Find a role by name from the roles endpoint and return its UUID.""" + resp = requests.get( + signoz.self.host_configs["8080"].get(ROLES_BASE), + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + assert resp.status_code == HTTPStatus.OK, resp.text + roles = resp.json()["data"] + role = next(r for r in roles if r["name"] == name) + return role["id"] + + +def create_service_account( + signoz: types.SigNoz, token: str, name: str, role: str = "signoz-viewer" +) -> str: + """Create a service account, assign a role, and return its ID.""" + resp = requests.post( + signoz.self.host_configs["8080"].get(SERVICE_ACCOUNT_BASE), + json={"name": name}, + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + assert resp.status_code == HTTPStatus.CREATED, resp.text + service_account_id = resp.json()["data"]["id"] + + role_id = find_role_by_name(signoz, token, role) + role_resp = requests.post( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/roles" + ), + json={"id": role_id}, + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + assert role_resp.status_code == HTTPStatus.NO_CONTENT, role_resp.text + + return service_account_id + + +def create_service_account_with_key( + signoz: types.SigNoz, token: str, name: str, role: str = "signoz-admin" +) -> tuple: + """Create a service account with an API key and return (service_account_id, api_key).""" + service_account_id = create_service_account(signoz, token, name, role) + + key_resp = requests.post( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/keys" + ), + json={"name": "auth-key", "expiresAt": 0}, + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + assert key_resp.status_code == HTTPStatus.CREATED, key_resp.text + api_key = key_resp.json()["data"]["key"] + + return service_account_id, api_key + + +def delete_service_account( + signoz: types.SigNoz, token: str, service_account_id: str +) -> None: + """Soft-delete a service account.""" + resp = requests.delete( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account_id}" + ), + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + assert resp.status_code == HTTPStatus.NO_CONTENT, resp.text + + +def find_service_account_by_name(signoz: types.SigNoz, token: str, name: str) -> dict: + """Find a service account by name from the list endpoint.""" + list_resp = requests.get( + signoz.self.host_configs["8080"].get(SERVICE_ACCOUNT_BASE), + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + assert list_resp.status_code == HTTPStatus.OK, list_resp.text + return next( + service_account + for service_account in list_resp.json()["data"] + if service_account["name"] == name + ) diff --git a/tests/integration/src/cloudintegrations/01_get_connection_params.py b/tests/integration/src/cloudintegrations/01_get_connection_params.py index e2f7571a06c..66ce3fe3551 100644 --- a/tests/integration/src/cloudintegrations/01_get_connection_params.py +++ b/tests/integration/src/cloudintegrations/01_get_connection_params.py @@ -159,3 +159,37 @@ def test_generate_connection_params( assert ( data["signoz_api_url"] == "https://test-deployment.test.signoz.cloud" ), "signoz_api_url should be https://test-deployment.test.signoz.cloud" + + # Verify the integration service account was created with viewer role, not admin. + # This guards against a privilege-escalation regression where the SA was + # previously created with admin access. + sa_list = requests.get( + signoz.self.host_configs["8080"].get("/api/v1/service_accounts"), + headers={"Authorization": f"Bearer {admin_token}"}, + timeout=5, + ) + assert sa_list.status_code == HTTPStatus.OK + + integration_sa = next( + (sa for sa in sa_list.json()["data"] if sa["name"] == "integration"), + None, + ) + assert integration_sa is not None, "Integration service account should exist" + + # Fetch roles via the dedicated roles endpoint + roles_resp = requests.get( + signoz.self.host_configs["8080"].get( + f"/api/v1/service_accounts/{integration_sa['id']}/roles" + ), + headers={"Authorization": f"Bearer {admin_token}"}, + timeout=5, + ) + assert roles_resp.status_code == HTTPStatus.OK, roles_resp.text + role_names = [role["name"] for role in roles_resp.json()["data"]] + + assert ( + "signoz-viewer" in role_names + ), f"Integration SA should have VIEWER role, got {role_names}" + assert ( + "signoz-admin" not in role_names + ), f"Integration SA must NOT have ADMIN role, got {role_names}" diff --git a/tests/integration/src/passwordauthn/03_apikey.py b/tests/integration/src/passwordauthn/03_apikey.py deleted file mode 100644 index c5473f3ed4c..00000000000 --- a/tests/integration/src/passwordauthn/03_apikey.py +++ /dev/null @@ -1,117 +0,0 @@ -from http import HTTPStatus -from typing import Callable - -import requests - -from fixtures import types - - -def test_api_key(signoz: types.SigNoz, get_token: Callable[[str, str], str]) -> None: - admin_token = get_token("admin@integration.test", "password123Z$") - - response = requests.post( - signoz.self.host_configs["8080"].get("/api/v1/pats"), - headers={"Authorization": f"Bearer {admin_token}"}, - json={ - "name": "admin", - "role": "ADMIN", - "expiresInDays": 1, - }, - timeout=2, - ) - - assert response.status_code == HTTPStatus.CREATED - pat_response = response.json() - assert "data" in pat_response - assert "token" in pat_response["data"] - - response = requests.get( - signoz.self.host_configs["8080"].get("/api/v1/user"), - timeout=2, - headers={"SIGNOZ-API-KEY": f"{pat_response["data"]["token"]}"}, - ) - - assert response.status_code == HTTPStatus.OK - - user_response = response.json() - found_user = next( - ( - user - for user in user_response["data"] - if user["email"] == "admin@integration.test" - ), - None, - ) - - response = requests.get( - signoz.self.host_configs["8080"].get("/api/v1/pats"), - headers={"SIGNOZ-API-KEY": f"{pat_response["data"]["token"]}"}, - timeout=2, - ) - - assert response.status_code == HTTPStatus.OK - assert "data" in response.json() - - found_pat = next( - (pat for pat in response.json()["data"] if pat["userId"] == found_user["id"]), - None, - ) - - assert found_pat is not None - assert found_pat["userId"] == found_user["id"] - assert found_pat["name"] == "admin" - assert found_pat["role"] == "ADMIN" - - -def test_api_key_role( - signoz: types.SigNoz, get_token: Callable[[str, str], str] -) -> None: - admin_token = get_token("admin@integration.test", "password123Z$") - - response = requests.post( - signoz.self.host_configs["8080"].get("/api/v1/pats"), - headers={"Authorization": f"Bearer {admin_token}"}, - json={ - "name": "viewer", - "role": "VIEWER", - "expiresInDays": 1, - }, - timeout=2, - ) - - assert response.status_code == HTTPStatus.CREATED - pat_response = response.json() - assert "data" in pat_response - assert "token" in pat_response["data"] - - response = requests.get( - signoz.self.host_configs["8080"].get("/api/v1/user"), - timeout=2, - headers={"SIGNOZ-API-KEY": f"{pat_response["data"]["token"]}"}, - ) - - assert response.status_code == HTTPStatus.FORBIDDEN - - response = requests.post( - signoz.self.host_configs["8080"].get("/api/v1/pats"), - headers={"Authorization": f"Bearer {admin_token}"}, - json={ - "name": "editor", - "role": "EDITOR", - "expiresInDays": 1, - }, - timeout=2, - ) - - assert response.status_code == HTTPStatus.CREATED - pat_response = response.json() - assert "data" in pat_response - assert "token" in pat_response["data"] - - response = requests.get( - signoz.self.host_configs["8080"].get("/api/v1/user"), - timeout=2, - headers={"SIGNOZ-API-KEY": f"{pat_response["data"]["token"]}"}, - ) - - assert response.status_code == HTTPStatus.FORBIDDEN diff --git a/tests/integration/src/passwordauthn/04_password.py b/tests/integration/src/passwordauthn/03_password.py similarity index 100% rename from tests/integration/src/passwordauthn/04_password.py rename to tests/integration/src/passwordauthn/03_password.py diff --git a/tests/integration/src/passwordauthn/05_role.py b/tests/integration/src/passwordauthn/04_role.py similarity index 75% rename from tests/integration/src/passwordauthn/05_role.py rename to tests/integration/src/passwordauthn/04_role.py index e065b7e066f..af0ec92bacd 100644 --- a/tests/integration/src/passwordauthn/05_role.py +++ b/tests/integration/src/passwordauthn/04_role.py @@ -1,5 +1,5 @@ from http import HTTPStatus -from typing import Callable, Tuple +from typing import Callable import requests @@ -9,7 +9,6 @@ def test_change_role( signoz: types.SigNoz, get_token: Callable[[str, str], str], - get_tokens: Callable[[str, str], Tuple[str, str]], ): admin_token = get_token("admin@integration.test", "password123Z$") @@ -35,9 +34,7 @@ def test_change_role( assert response.status_code == HTTPStatus.NO_CONTENT # Make some API calls as new user - new_user_token, new_user_refresh_token = get_tokens( - "admin+rolechange@integration.test", "password123Z$" - ) + new_user_token = get_token("admin+rolechange@integration.test", "password123Z$") response = requests.get( signoz.self.host_configs["8080"].get("/api/v1/user/me"), @@ -78,27 +75,8 @@ def test_change_role( headers={"Authorization": f"Bearer {new_user_token}"}, ) - assert response.status_code == HTTPStatus.UNAUTHORIZED - - # Rotate token for new user - response = requests.post( - signoz.self.host_configs["8080"].get("/api/v2/sessions/rotate"), - json={ - "refreshToken": new_user_refresh_token, - }, - headers={"Authorization": f"Bearer {new_user_token}"}, - timeout=2, - ) - assert response.status_code == HTTPStatus.OK - # Make some API call again which is protected - rotate_response = response.json()["data"] - new_user_token, new_user_refresh_token = ( - rotate_response["accessToken"], - rotate_response["refreshToken"], - ) - response = requests.get( signoz.self.host_configs["8080"].get("/api/v1/org/preferences"), timeout=2, diff --git a/tests/integration/src/passwordauthn/06_duplicate_invite.py b/tests/integration/src/passwordauthn/05_duplicate_invite.py similarity index 100% rename from tests/integration/src/passwordauthn/06_duplicate_invite.py rename to tests/integration/src/passwordauthn/05_duplicate_invite.py diff --git a/tests/integration/src/passwordauthn/07_invite_status.py b/tests/integration/src/passwordauthn/06_invite_status.py similarity index 100% rename from tests/integration/src/passwordauthn/07_invite_status.py rename to tests/integration/src/passwordauthn/06_invite_status.py diff --git a/tests/integration/src/passwordauthn/08_user_unique_index.py b/tests/integration/src/passwordauthn/07_user_unique_index.py similarity index 100% rename from tests/integration/src/passwordauthn/08_user_unique_index.py rename to tests/integration/src/passwordauthn/07_user_unique_index.py diff --git a/tests/integration/src/serviceaccount/01_crud.py b/tests/integration/src/serviceaccount/01_crud.py new file mode 100644 index 00000000000..8226f11a2ee --- /dev/null +++ b/tests/integration/src/serviceaccount/01_crud.py @@ -0,0 +1,244 @@ +from http import HTTPStatus +from typing import Callable + +import requests + +from fixtures import types +from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD +from fixtures.logger import setup_logger +from fixtures.serviceaccount import ( + SERVICE_ACCOUNT_BASE, + create_service_account, + delete_service_account, + find_service_account_by_name, +) + +logger = setup_logger(__name__) + + +def test_create_service_account( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + + response = requests.post( + signoz.self.host_configs["8080"].get(SERVICE_ACCOUNT_BASE), + json={"name": "test-sa"}, + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + + assert response.status_code == HTTPStatus.CREATED, response.text + data = response.json()["data"] + assert "id" in data + assert len(data["id"]) > 0 + + +def test_create_service_account_invalid_name( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + + # name with spaces should be rejected + response = requests.post( + signoz.self.host_configs["8080"].get(SERVICE_ACCOUNT_BASE), + json={"name": "invalid name"}, + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + + assert response.status_code == HTTPStatus.BAD_REQUEST, response.text + + +def test_create_service_account_missing_name( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + """Creating a service account with an empty name should be rejected.""" + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + + response = requests.post( + signoz.self.host_configs["8080"].get(SERVICE_ACCOUNT_BASE), + json={"name": ""}, + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + + assert response.status_code == HTTPStatus.BAD_REQUEST, response.text + + +def test_list_service_accounts( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + + response = requests.get( + signoz.self.host_configs["8080"].get(SERVICE_ACCOUNT_BASE), + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + + assert response.status_code == HTTPStatus.OK, response.text + data = response.json()["data"] + assert isinstance(data, list) + + # should contain the SA we created in the earlier test + names = [service_account["name"] for service_account in data] + assert "test-sa" in names + + +def test_get_service_account( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + service_account = find_service_account_by_name(signoz, token, "test-sa") + + response = requests.get( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account['id']}" + ), + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + + assert response.status_code == HTTPStatus.OK, response.text + data = response.json()["data"] + assert data["id"] == service_account["id"] + assert data["name"] == "test-sa" + assert data["status"] == "active" + assert "email" in data + assert "serviceAccountRoles" in data + + +def test_get_service_account_not_found( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + + response = requests.get( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/00000000-0000-0000-0000-000000000000" + ), + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + + assert response.status_code == HTTPStatus.NOT_FOUND, response.text + + +def test_update_service_account( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + service_account = find_service_account_by_name(signoz, token, "test-sa") + + response = requests.put( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account['id']}" + ), + json={"name": "test-sa-updated"}, + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + + assert response.status_code == HTTPStatus.NO_CONTENT, response.text + + # verify the update + get_resp = requests.get( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account['id']}" + ), + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + assert get_resp.json()["data"]["name"] == "test-sa-updated" + + +def test_delete_service_account( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + service_account_id = create_service_account(signoz, token, "sa-to-disable") + + delete_service_account(signoz, token, service_account_id) + + # verify status changed to deleted + get_resp = requests.get( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account_id}" + ), + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + assert get_resp.json()["data"]["status"] == "deleted" + + +def test_create_after_delete_reuses_name( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + """The partial unique index on (name, org_id) excludes deleted rows, + so create → delete → create with the same name must succeed.""" + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + service_account_name = "sa-reuse-name" + + # 1. create + first_id = create_service_account(signoz, token, service_account_name) + + # 2. creating again with the same name should fail (conflict) + dup_resp = requests.post( + signoz.self.host_configs["8080"].get(SERVICE_ACCOUNT_BASE), + json={"name": service_account_name}, + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + assert dup_resp.status_code == HTTPStatus.CONFLICT, dup_resp.text + + # 3. soft-delete the first one + delete_service_account(signoz, token, first_id) + + # 4. now creating with the same name should succeed + second_id = create_service_account(signoz, token, service_account_name) + assert second_id != first_id, "New SA should have a different ID" + + +def test_delete_already_deleted_service_account( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + """Deleting an already-deleted service account should be handled gracefully.""" + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + service_account_id = create_service_account(signoz, token, "sa-double-delete") + + # first delete + delete_service_account(signoz, token, service_account_id) + + # second delete should be handled gracefully (idempotent or error) + response = requests.delete( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account_id}" + ), + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + + assert ( + response.status_code == HTTPStatus.NOT_IMPLEMENTED + ), f"Expected 501 for already-deleted SA, got {response.status_code}: {response.text}" diff --git a/tests/integration/src/serviceaccount/02_keys.py b/tests/integration/src/serviceaccount/02_keys.py new file mode 100644 index 00000000000..5c5a71d77cd --- /dev/null +++ b/tests/integration/src/serviceaccount/02_keys.py @@ -0,0 +1,300 @@ +import time +from http import HTTPStatus +from typing import Callable + +import requests + +from fixtures import types +from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD +from fixtures.logger import setup_logger +from fixtures.serviceaccount import ( + SERVICE_ACCOUNT_BASE, + create_service_account, + find_service_account_by_name, +) + +logger = setup_logger(__name__) + + +def test_create_api_key( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + service_account_id = create_service_account(signoz, token, "sa-for-keys") + + response = requests.post( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/keys" + ), + json={"name": "my-key", "expiresAt": 0}, + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + + assert response.status_code == HTTPStatus.CREATED, response.text + data = response.json()["data"] + assert "id" in data + assert "key" in data + assert len(data["key"]) > 0 + + +def test_create_api_key_duplicate_name( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + service_account = find_service_account_by_name(signoz, token, "sa-for-keys") + + # creating a key with the same name should fail + response = requests.post( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account['id']}/keys" + ), + json={"name": "my-key", "expiresAt": 0}, + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + + assert response.status_code == HTTPStatus.CONFLICT, response.text + + +def test_list_api_keys( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + service_account = find_service_account_by_name(signoz, token, "sa-for-keys") + + response = requests.get( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account['id']}/keys" + ), + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + + assert response.status_code == HTTPStatus.OK, response.text + data = response.json()["data"] + assert isinstance(data, list) + assert len(data) >= 1 + + key_entry = data[0] + assert "id" in key_entry + assert "name" in key_entry + assert key_entry["name"] == "my-key" + + +def test_update_api_key( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + service_account = find_service_account_by_name(signoz, token, "sa-for-keys") + + keys_resp = requests.get( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account['id']}/keys" + ), + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + key_id = keys_resp.json()["data"][0]["id"] + + response = requests.put( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account['id']}/keys/{key_id}" + ), + json={"name": "renamed-key", "expiresAt": 0}, + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + + assert response.status_code == HTTPStatus.NO_CONTENT, response.text + + +def test_revoke_api_key( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + service_account_id = create_service_account(signoz, token, "sa-revoke-key") + + create_resp = requests.post( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/keys" + ), + json={"name": "key-to-revoke", "expiresAt": 0}, + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + assert create_resp.status_code == HTTPStatus.CREATED + key_id = create_resp.json()["data"]["id"] + + response = requests.delete( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/keys/{key_id}" + ), + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + + assert response.status_code == HTTPStatus.NO_CONTENT, response.text + + +def test_create_api_key_with_expiry( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + """Key created with a future expiresAt should be usable.""" + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + service_account_id = create_service_account(signoz, token, "sa-key-expiry") + + future_ts = int(time.time()) + 3600 # 1 hour from now + response = requests.post( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/keys" + ), + json={"name": "future-key", "expiresAt": future_ts}, + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + + assert response.status_code == HTTPStatus.CREATED, response.text + api_key = response.json()["data"]["key"] + + # key should work since it hasn't expired + dash_resp = requests.get( + signoz.self.host_configs["8080"].get("/api/v1/dashboards"), + headers={"SIGNOZ-API-KEY": api_key}, + timeout=5, + ) + assert dash_resp.status_code == HTTPStatus.OK, dash_resp.text + + # verify expiresAt is stored correctly + keys_resp = requests.get( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/keys" + ), + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + key_entry = next(k for k in keys_resp.json()["data"] if k["name"] == "future-key") + assert key_entry["expiresAt"] == future_ts + + +def test_create_api_key_with_past_expiry_rejected( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + """Creating a key with an already-past expiresAt should be rejected.""" + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + service_account_id = create_service_account(signoz, token, "sa-key-past-expiry") + + past_ts = int(time.time()) - 60 # 1 minute ago + response = requests.post( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/keys" + ), + json={"name": "expired-key", "expiresAt": past_ts}, + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + + assert ( + response.status_code == HTTPStatus.BAD_REQUEST + ), f"Expected 400 for past expiresAt, got {response.status_code}: {response.text}" + + +def test_create_api_key_no_expiry( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + """Key with expiresAt=0 should never expire.""" + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + service_account_id = create_service_account(signoz, token, "sa-key-no-expiry") + + response = requests.post( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/keys" + ), + json={"name": "forever-key", "expiresAt": 0}, + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + + assert response.status_code == HTTPStatus.CREATED, response.text + api_key = response.json()["data"]["key"] + + dash_resp = requests.get( + signoz.self.host_configs["8080"].get("/api/v1/dashboards"), + headers={"SIGNOZ-API-KEY": api_key}, + timeout=5, + ) + assert dash_resp.status_code == HTTPStatus.OK, dash_resp.text + + # verify expiresAt is 0 + keys_resp = requests.get( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/keys" + ), + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + key_entry = next(k for k in keys_resp.json()["data"] if k["name"] == "forever-key") + assert key_entry["expiresAt"] == 0 + + +def test_update_api_key_expiry( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + """Updating expiresAt to a past value should be rejected.""" + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + service_account_id = create_service_account(signoz, token, "sa-key-update-expiry") + + # create with no expiry + create_resp = requests.post( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/keys" + ), + json={"name": "update-expiry-key", "expiresAt": 0}, + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + assert create_resp.status_code == HTTPStatus.CREATED + key_id = create_resp.json()["data"]["id"] + api_key = create_resp.json()["data"]["key"] + + # updating to expire in the past should be rejected + past_ts = int(time.time()) - 60 + update_resp = requests.put( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/keys/{key_id}" + ), + json={"name": "update-expiry-key", "expiresAt": past_ts}, + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + assert ( + update_resp.status_code == HTTPStatus.BAD_REQUEST + ), f"Expected 400 for past expiresAt update, got {update_resp.status_code}: {update_resp.text}" + + # key should still work since the update was rejected + dash_resp = requests.get( + signoz.self.host_configs["8080"].get("/api/v1/dashboards"), + headers={"SIGNOZ-API-KEY": api_key}, + timeout=5, + ) + assert ( + dash_resp.status_code == HTTPStatus.OK + ), f"Key should still work after rejected update, got {dash_resp.status_code}: {dash_resp.text}" diff --git a/tests/integration/src/serviceaccount/03_auth.py b/tests/integration/src/serviceaccount/03_auth.py new file mode 100644 index 00000000000..97ecd0ed99b --- /dev/null +++ b/tests/integration/src/serviceaccount/03_auth.py @@ -0,0 +1,339 @@ +from http import HTTPStatus +from typing import Callable + +import requests + +from fixtures import types +from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD +from fixtures.logger import setup_logger +from fixtures.serviceaccount import ( + SERVICE_ACCOUNT_BASE, + create_service_account_with_key, + delete_service_account, +) + +logger = setup_logger(__name__) + + +def test_service_account_key_auth_on_dashboards( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + """Service account API key with admin role can access dashboards.""" + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + _, api_key = create_service_account_with_key(signoz, token, "sa-dashboard-test") + + response = requests.get( + signoz.self.host_configs["8080"].get("/api/v1/dashboards"), + headers={"SIGNOZ-API-KEY": api_key}, + timeout=5, + ) + + assert response.status_code == HTTPStatus.OK, response.text + + +def test_service_account_key_forbidden_on_user_me( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + """Service account key must not access /api/v1/user/me — it's user-only.""" + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + _, api_key = create_service_account_with_key(signoz, token, "sa-user-me-test") + + response = requests.get( + signoz.self.host_configs["8080"].get("/api/v1/user/me"), + headers={"SIGNOZ-API-KEY": api_key}, + timeout=5, + ) + + ## This shouldn't be allowed on api key identn, will be updated once we fix that. + assert ( + response.status_code == HTTPStatus.NOT_FOUND + ), f"Expected 404 for service account on /user/me, got {response.status_code}: {response.text}" + + +def test_service_account_key_forbidden_on_user_preferences( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + """Service account key must not access user preference endpoints.""" + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + _, api_key = create_service_account_with_key(signoz, token, "sa-pref-test") + + response = requests.get( + signoz.self.host_configs["8080"].get("/api/v1/user/preferences"), + headers={"SIGNOZ-API-KEY": api_key}, + timeout=5, + ) + + ## This shouldn't be allowed on api key identn, will be updated once we fix that. + assert ( + response.status_code == HTTPStatus.OK + ), f"Expected 200 for service account on /user/preferences, got {response.status_code}: {response.text}" + + +def test_service_account_role_access_admin( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + """Admin service account can access admin, edit, and view endpoints.""" + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + _, api_key = create_service_account_with_key( + signoz, token, "sa-role-admin", role="signoz-admin" + ) + + # AdminAccess: list service accounts + resp = requests.get( + signoz.self.host_configs["8080"].get(SERVICE_ACCOUNT_BASE), + headers={"SIGNOZ-API-KEY": api_key}, + timeout=5, + ) + assert ( + resp.status_code == HTTPStatus.OK + ), f"Admin service account should access admin endpoint, got {resp.status_code}: {resp.text}" + + # EditAccess: create a dashboard + resp = requests.post( + signoz.self.host_configs["8080"].get("/api/v1/dashboards"), + json={ + "title": "admin-sa-dash", + "uploadedGrafana": False, + "version": "v4", + }, + headers={"SIGNOZ-API-KEY": api_key}, + timeout=5, + ) + assert ( + resp.status_code == HTTPStatus.CREATED + ), f"Admin service account should access edit endpoint, got {resp.status_code}: {resp.text}" + + # ViewAccess: list dashboards + resp = requests.get( + signoz.self.host_configs["8080"].get("/api/v1/dashboards"), + headers={"SIGNOZ-API-KEY": api_key}, + timeout=5, + ) + assert ( + resp.status_code == HTTPStatus.OK + ), f"Admin service account should access view endpoint, got {resp.status_code}: {resp.text}" + + +def test_service_account_role_access_editor( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + """Editor service account can access edit and view endpoints but not admin.""" + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + _, api_key = create_service_account_with_key( + signoz, token, "sa-role-editor", role="signoz-editor" + ) + + # AdminAccess: should be forbidden + resp = requests.get( + signoz.self.host_configs["8080"].get(SERVICE_ACCOUNT_BASE), + headers={"SIGNOZ-API-KEY": api_key}, + timeout=5, + ) + assert ( + resp.status_code == HTTPStatus.FORBIDDEN + ), f"Editor service account should be forbidden from admin endpoint, got {resp.status_code}: {resp.text}" + + # EditAccess: create a dashboard + resp = requests.post( + signoz.self.host_configs["8080"].get("/api/v1/dashboards"), + json={ + "title": "editor-sa-dash", + "uploadedGrafana": False, + "version": "v4", + }, + headers={"SIGNOZ-API-KEY": api_key}, + timeout=5, + ) + assert ( + resp.status_code == HTTPStatus.CREATED + ), f"Editor service account should access edit endpoint, got {resp.status_code}: {resp.text}" + + # ViewAccess: list dashboards + resp = requests.get( + signoz.self.host_configs["8080"].get("/api/v1/dashboards"), + headers={"SIGNOZ-API-KEY": api_key}, + timeout=5, + ) + assert ( + resp.status_code == HTTPStatus.OK + ), f"Editor service account should access view endpoint, got {resp.status_code}: {resp.text}" + + +def test_service_account_role_access_viewer( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + """Viewer service account can access view endpoints but not edit or admin.""" + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + _, api_key = create_service_account_with_key( + signoz, token, "sa-role-viewer", role="signoz-viewer" + ) + + # AdminAccess: should be forbidden + resp = requests.get( + signoz.self.host_configs["8080"].get(SERVICE_ACCOUNT_BASE), + headers={"SIGNOZ-API-KEY": api_key}, + timeout=5, + ) + assert ( + resp.status_code == HTTPStatus.FORBIDDEN + ), f"Viewer service account should be forbidden from admin endpoint, got {resp.status_code}: {resp.text}" + + # EditAccess: should be forbidden + resp = requests.post( + signoz.self.host_configs["8080"].get("/api/v1/dashboards"), + json={ + "title": "viewer-sa-dash", + "uploadedGrafana": False, + "version": "v4", + }, + headers={"SIGNOZ-API-KEY": api_key}, + timeout=5, + ) + assert ( + resp.status_code == HTTPStatus.FORBIDDEN + ), f"Viewer service account should be forbidden from edit endpoint, got {resp.status_code}: {resp.text}" + + # ViewAccess: list dashboards + resp = requests.get( + signoz.self.host_configs["8080"].get("/api/v1/dashboards"), + headers={"SIGNOZ-API-KEY": api_key}, + timeout=5, + ) + assert ( + resp.status_code == HTTPStatus.OK + ), f"Viewer service account should access view endpoint, got {resp.status_code}: {resp.text}" + + +def test_service_account_key_deleted_account_rejected( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + """A deleted service account's key must be rejected.""" + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + service_account_id, api_key = create_service_account_with_key( + signoz, token, "sa-disable-auth" + ) + + # verify the key works before deleting + response = requests.get( + signoz.self.host_configs["8080"].get("/api/v1/dashboards"), + headers={"SIGNOZ-API-KEY": api_key}, + timeout=5, + ) + assert response.status_code == HTTPStatus.OK + + # soft-delete the SA + delete_service_account(signoz, token, service_account_id) + + # now the key should be rejected + response = requests.get( + signoz.self.host_configs["8080"].get("/api/v1/dashboards"), + headers={"SIGNOZ-API-KEY": api_key}, + timeout=5, + ) + + assert response.status_code in ( + HTTPStatus.UNAUTHORIZED, + HTTPStatus.FORBIDDEN, + ), f"Expected 401/403 for disabled service account, got {response.status_code}: {response.text}" + + +def test_service_account_key_revoked_key_rejected( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + """A revoked API key must be rejected.""" + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + service_account_id, api_key = create_service_account_with_key( + signoz, token, "sa-revoke-auth" + ) + + # verify the key works first + response = requests.get( + signoz.self.host_configs["8080"].get("/api/v1/dashboards"), + headers={"SIGNOZ-API-KEY": api_key}, + timeout=5, + ) + assert response.status_code == HTTPStatus.OK + + # find the key id + keys_resp = requests.get( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/keys" + ), + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + key_id = keys_resp.json()["data"][0]["id"] + + # revoke it + revoke_resp = requests.delete( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/keys/{key_id}" + ), + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + assert revoke_resp.status_code == HTTPStatus.NO_CONTENT + + # now the key should be rejected + response = requests.get( + signoz.self.host_configs["8080"].get("/api/v1/dashboards"), + headers={"SIGNOZ-API-KEY": api_key}, + timeout=5, + ) + + assert ( + response.status_code == HTTPStatus.UNAUTHORIZED + ), f"Expected 401 for revoked key, got {response.status_code}: {response.text}" + + +def test_user_token_still_works_on_user_me( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + """Verify that normal user JWT tokens still work on user-only endpoints.""" + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + + response = requests.get( + signoz.self.host_configs["8080"].get("/api/v1/user/me"), + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + + assert response.status_code == HTTPStatus.OK, response.text + data = response.json()["data"] + assert data["email"] == USER_ADMIN_EMAIL + + +def test_user_token_still_works_on_user_preferences( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + """Verify that normal user JWT tokens still work on preference endpoints.""" + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + + response = requests.get( + signoz.self.host_configs["8080"].get("/api/v1/user/preferences"), + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + + assert response.status_code == HTTPStatus.OK, response.text + assert response.json()["data"] is not None diff --git a/tests/integration/src/serviceaccount/04_roles.py b/tests/integration/src/serviceaccount/04_roles.py new file mode 100644 index 00000000000..4e53f5da181 --- /dev/null +++ b/tests/integration/src/serviceaccount/04_roles.py @@ -0,0 +1,195 @@ +from http import HTTPStatus +from typing import Callable + +import requests + +from fixtures import types +from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD +from fixtures.logger import setup_logger +from fixtures.serviceaccount import ( + SERVICE_ACCOUNT_BASE, + create_service_account, + create_service_account_with_key, + find_role_by_name, +) + +logger = setup_logger(__name__) + + +def test_get_service_account_roles( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + """GET /{id}/roles returns the assigned roles list.""" + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + service_account_id = create_service_account( + signoz, token, "sa-get-roles", role="signoz-viewer" + ) + + response = requests.get( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/roles" + ), + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + + assert response.status_code == HTTPStatus.OK, response.text + data = response.json()["data"] + assert isinstance(data, list) + assert len(data) >= 1 + role_names = [r["name"] for r in data] + assert "signoz-viewer" in role_names + + +def test_assign_role_to_service_account( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + """POST /{id}/roles assigns a new role, verify via GET.""" + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + + # create service account with viewer role + service_account_id = create_service_account( + signoz, token, "sa-assign-role", role="signoz-viewer" + ) + + # assign editor role additionally + editor_role_id = find_role_by_name(signoz, token, "signoz-editor") + assign_resp = requests.post( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/roles" + ), + json={"id": editor_role_id}, + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + assert assign_resp.status_code == HTTPStatus.NO_CONTENT, assign_resp.text + + # verify both roles are present + roles_resp = requests.get( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/roles" + ), + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + assert roles_resp.status_code == HTTPStatus.OK, roles_resp.text + role_names = [r["name"] for r in roles_resp.json()["data"]] + assert "signoz-viewer" in role_names + assert "signoz-editor" in role_names + + +def test_assign_role_idempotent( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + """POST same role twice succeeds (store uses ON CONFLICT DO NOTHING).""" + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + service_account_id = create_service_account( + signoz, token, "sa-role-idempotent", role="signoz-viewer" + ) + + viewer_role_id = find_role_by_name(signoz, token, "signoz-viewer") + + # assign the same role again + resp = requests.post( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/roles" + ), + json={"id": viewer_role_id}, + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + assert resp.status_code == HTTPStatus.NO_CONTENT, resp.text + + # verify only one instance of the role + roles_resp = requests.get( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/roles" + ), + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + role_names = [r["name"] for r in roles_resp.json()["data"]] + assert role_names.count("signoz-viewer") == 1 + + +def test_remove_role_from_service_account( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + """DELETE /{id}/roles/{rid} revokes a role.""" + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + service_account_id = create_service_account( + signoz, token, "sa-remove-role", role="signoz-editor" + ) + + editor_role_id = find_role_by_name(signoz, token, "signoz-editor") + + # remove the role + resp = requests.delete( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/roles/{editor_role_id}" + ), + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + assert resp.status_code == HTTPStatus.NO_CONTENT, resp.text + + # verify role is gone + roles_resp = requests.get( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/roles" + ), + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + assert roles_resp.status_code == HTTPStatus.OK, roles_resp.text + role_names = [r["name"] for r in roles_resp.json()["data"]] + assert "signoz-editor" not in role_names + + +def test_remove_role_verify_access_lost( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + """After role removal, service account key gets 403 on endpoints requiring that role.""" + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + service_account_id, api_key = create_service_account_with_key( + signoz, token, "sa-role-access-lost", role="signoz-admin" + ) + + # verify admin access works + resp = requests.get( + signoz.self.host_configs["8080"].get(SERVICE_ACCOUNT_BASE), + headers={"SIGNOZ-API-KEY": api_key}, + timeout=5, + ) + assert resp.status_code == HTTPStatus.OK, resp.text + + # remove admin role + admin_role_id = find_role_by_name(signoz, token, "signoz-admin") + del_resp = requests.delete( + signoz.self.host_configs["8080"].get( + f"{SERVICE_ACCOUNT_BASE}/{service_account_id}/roles/{admin_role_id}" + ), + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + assert del_resp.status_code == HTTPStatus.NO_CONTENT, del_resp.text + + # now admin endpoint should be forbidden + resp = requests.get( + signoz.self.host_configs["8080"].get(SERVICE_ACCOUNT_BASE), + headers={"SIGNOZ-API-KEY": api_key}, + timeout=5, + ) + assert ( + resp.status_code == HTTPStatus.FORBIDDEN + ), f"Expected 403 after role removal, got {resp.status_code}: {resp.text}" diff --git a/tests/integration/src/serviceaccount/05_me.py b/tests/integration/src/serviceaccount/05_me.py new file mode 100644 index 00000000000..bd78d0b22aa --- /dev/null +++ b/tests/integration/src/serviceaccount/05_me.py @@ -0,0 +1,112 @@ +from http import HTTPStatus +from typing import Callable + +import requests + +from fixtures import types +from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD +from fixtures.logger import setup_logger +from fixtures.serviceaccount import ( + SERVICE_ACCOUNT_BASE, + create_service_account_with_key, +) + +logger = setup_logger(__name__) + +ME_ENDPOINT = f"{SERVICE_ACCOUNT_BASE}/me" + + +def test_get_my_service_account( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + """Service account with API key calls GET /me, gets own details.""" + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + service_account_id, api_key = create_service_account_with_key( + signoz, token, "sa-me-get" + ) + + response = requests.get( + signoz.self.host_configs["8080"].get(ME_ENDPOINT), + headers={"SIGNOZ-API-KEY": api_key}, + timeout=5, + ) + + assert response.status_code == HTTPStatus.OK, response.text + data = response.json()["data"] + assert data["id"] == service_account_id + assert data["name"] == "sa-me-get" + + +def test_get_me_requires_sa_identity( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + """User JWT on GET /me should fail — no service account identity in claims.""" + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + + response = requests.get( + signoz.self.host_configs["8080"].get(ME_ENDPOINT), + headers={"Authorization": f"Bearer {token}"}, + timeout=5, + ) + + # user JWT has no service account ID in claims, should fail + assert response.status_code in ( + HTTPStatus.BAD_REQUEST, + HTTPStatus.FORBIDDEN, + HTTPStatus.NOT_FOUND, + HTTPStatus.UNAUTHORIZED, + ), f"Expected error for user JWT on service account /me, got {response.status_code}: {response.text}" + + +def test_update_my_service_account( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + """Service account calls PUT /me with new name, verify update.""" + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + service_account_id, api_key = create_service_account_with_key( + signoz, token, "sa-me-update" + ) + + response = requests.put( + signoz.self.host_configs["8080"].get(ME_ENDPOINT), + json={"name": "sa-me-updated"}, + headers={"SIGNOZ-API-KEY": api_key}, + timeout=5, + ) + + assert response.status_code == HTTPStatus.NO_CONTENT, response.text + + # verify the update via GET /me + get_resp = requests.get( + signoz.self.host_configs["8080"].get(ME_ENDPOINT), + headers={"SIGNOZ-API-KEY": api_key}, + timeout=5, + ) + assert get_resp.status_code == HTTPStatus.OK, get_resp.text + assert get_resp.json()["data"]["name"] == "sa-me-updated" + assert get_resp.json()["data"]["id"] == service_account_id + + +def test_update_me_invalid_name_rejected( + signoz: types.SigNoz, + create_user_admin: types.Operation, # pylint: disable=unused-argument + get_token: Callable[[str, str], str], +): + """Service account calls PUT /me with invalid name, gets 400.""" + token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD) + _, api_key = create_service_account_with_key(signoz, token, "sa-me-invalid") + + response = requests.put( + signoz.self.host_configs["8080"].get(ME_ENDPOINT), + json={"name": "invalid name with spaces"}, + headers={"SIGNOZ-API-KEY": api_key}, + timeout=5, + ) + + assert response.status_code == HTTPStatus.BAD_REQUEST, response.text diff --git a/tests/integration/src/serviceaccount/__init__.py b/tests/integration/src/serviceaccount/__init__.py new file mode 100644 index 00000000000..e69de29bb2d From 42415e08739c4e8237e7856c0661f30638d68cd1 Mon Sep 17 00:00:00 2001 From: Pandey Date: Wed, 1 Apr 2026 15:40:52 +0530 Subject: [PATCH 67/78] feat(audit): handler-level AuditDef, audit middleware, and response capture (#10791) * feat(audit): handler-level AuditDef and response-capturing wrapper Add declarative audit instrumentation to the handler package. Routes declare an AuditDef alongside OpenAPIDef; the handler automatically captures the response status/body and emits an audit event via auditor.Audit() after every request. * refactor(audit): move audit logic to middleware, merge with logging Move audit event emission from handler to middleware layer. The handler package keeps only the AuditDef struct and AuditDefProvider interface. The logging middleware now handles both request logging and audit event emission using a single response capture, avoiding double-wrapping. Rename badResponseLoggingWriter to responseCapture with body capture on all 4xx/5xx responses (previously only 400 and 5xx). * refactor(audit): rename Logging middleware to Audit, merge into single file Delete logging.go and merge its contents into audit.go. Rename Logging/NewLogging to Audit/NewAudit. The response.go file with responseCapture is unchanged. * refactor(audit): extract NewAuditEventFromHTTPRequest factory into audittypes Move event construction to audittypes.NewAuditEventFromHTTPRequest with an AuditEventContext struct for caller-provided fields. The audittypes layer reads only transport fields from *http.Request and has no mux, authtypes, or context dependencies. The middleware pre-extracts principal, trace, error, and route fields before calling the factory. * refactor(audit): move error parsing to render.ErrorFromBody and render.ErrorTypeFromStatusCode Add render.ErrorFromBody to extract errors.JSON from a JSON-encoded ErrorResponse body, and render.ErrorTypeFromStatusCode to reverse-map HTTP status codes to error type strings. The middleware now uses these instead of local duplicates. * refactor(audit): move AuditDef onto Handler interface, consolidate files Move AuditDef() onto the Handler interface directly. All Handler implementations now carry it: handler returns the configured def, healthOpenAPIHandler returns nil. Delete the separate AuditDefProvider interface and audit.go handler file. Move excludedRoutes check before audit emission so excluded routes skip both logging and audit. * feat(audit): add option.go with AuditDef, Option, and WithAuditDef * refactor(audit): decompose AuditEvent into attribute sub-structs, add tests Decompose flat AuditEvent fields into typed sub-structs (AuditEventAuditAttributes, PrincipalAttributes, ResourceAttributes, ErrorAttributes, TransportAttributes) each with a constructor and Put(pcommon.Map) method. Simplify NewAuditEventFromHTTPRequest to accept authtypes.Claims and oteltrace IDs directly. Simplify the middleware caller accordingly. Add unit tests for the factory, outcome boundary, and principal type derivation. * refactor(audit): shorten attribute struct names, drop error message Rename AuditEventAuditAttributes to AuditAttributes, AuditEventPrincipalAttributes to PrincipalAttributes, and likewise for Resource, Error, and Transport. The package prefix already disambiguates. Remove ErrorMessage from ErrorAttributes to avoid leaking sensitive or PII data into audit logs. Error type and code are sufficient for filtering; investigators can correlate via trace ID. * fix(audit): update auditorserver test and otlphttp provider for new struct layout Update newTestEvent in server_test.go to use nested AuditAttributes and ResourceAttributes. Update otlphttpauditor provider to access PrincipalOrgID via PrincipalAttributes. Fix godot lint on attribute section comments. * fix(audit): fix gjson path in ErrorCodeFromBody, add tests Fix ErrorCodeFromBody gjson path from "errors.code" to "error.code" to match the ErrorResponse JSON structure. Add unit tests for valid error response and invalid JSON cases. * fix(audit): add CodeUnset, use ErrorCodeFromBody in middleware Add errors.CodeUnset for responses missing an error code. Update the audit middleware to use render.ErrorCodeFromBody instead of the removed render.ErrorFromBody. * test(audit): add unit tests for responseCapture Test the four meaningful behaviors: success responses don't capture body, error responses capture body, large error bodies truncate at 4096 bytes, and 204 No Content suppresses writes entirely. * fix(audit): check rw.Write return values in response_test.go * style(audit): rename want prefix to expected in test fields * refactor(audit): replace Sprintf with strings.Builder in newBody Handle edge cases where principal email, ID, or resource ID may be empty. The builder conditionally includes each segment, avoiding empty parentheses or leading spaces in the audit body. Add test cases covering all meaningful combinations: success/failure with full/partial/empty principal, resource ID, and error details. * chore: fix formatting * chore: remove json tags * fix: rebase with main --- ee/auditor/otlphttpauditor/provider.go | 4 +- ee/query-service/app/server.go | 2 +- pkg/apiserver/signozapiserver/registry.go | 5 + pkg/auditor/auditorserver/server_test.go | 14 +- pkg/errors/code.go | 6 + pkg/http/handler/handler.go | 15 +- pkg/http/handler/option.go | 24 +++ pkg/http/middleware/audit.go | 169 ++++++++++++++++++ pkg/http/middleware/logging.go | 81 --------- pkg/http/middleware/response.go | 111 ++++++++---- pkg/http/middleware/response_test.go | 88 +++++++++ pkg/http/render/render.go | 40 +++++ pkg/http/render/render_test.go | 25 +++ pkg/query-service/app/server.go | 2 +- pkg/types/audittypes/attributes.go | 206 ++++++++++++++++++++++ pkg/types/audittypes/attributes_test.go | 203 +++++++++++++++++++++ pkg/types/audittypes/event.go | 199 ++++++++++----------- pkg/types/audittypes/event_test.go | 99 +++++++++++ 18 files changed, 1054 insertions(+), 239 deletions(-) create mode 100644 pkg/http/handler/option.go create mode 100644 pkg/http/middleware/audit.go delete mode 100644 pkg/http/middleware/logging.go create mode 100644 pkg/http/middleware/response_test.go create mode 100644 pkg/types/audittypes/attributes.go create mode 100644 pkg/types/audittypes/attributes_test.go create mode 100644 pkg/types/audittypes/event_test.go diff --git a/ee/auditor/otlphttpauditor/provider.go b/ee/auditor/otlphttpauditor/provider.go index 89e67bb56ae..c73c2715987 100644 --- a/ee/auditor/otlphttpauditor/provider.go +++ b/ee/auditor/otlphttpauditor/provider.go @@ -76,12 +76,12 @@ func (provider *provider) Start(ctx context.Context) error { } func (provider *provider) Audit(ctx context.Context, event audittypes.AuditEvent) { - if event.PrincipalOrgID.IsZero() { + if event.PrincipalAttributes.PrincipalOrgID.IsZero() { provider.settings.Logger().WarnContext(ctx, "audit event dropped as org_id is zero") return } - if _, err := provider.licensing.GetActive(ctx, event.PrincipalOrgID); err != nil { + if _, err := provider.licensing.GetActive(ctx, event.PrincipalAttributes.PrincipalOrgID); err != nil { return } diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index 79ab2999e52..274da36c793 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -229,7 +229,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h s.config.APIServer.Timeout.Default, s.config.APIServer.Timeout.Max, ).Wrap) - r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap) + r.Use(middleware.NewAudit(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes, nil).Wrap) r.Use(middleware.NewComment().Wrap) apiHandler.RegisterRoutes(r, am) diff --git a/pkg/apiserver/signozapiserver/registry.go b/pkg/apiserver/signozapiserver/registry.go index 997c7d872a3..b028eec08ad 100644 --- a/pkg/apiserver/signozapiserver/registry.go +++ b/pkg/apiserver/signozapiserver/registry.go @@ -50,6 +50,11 @@ func (handler *healthOpenAPIHandler) ServeOpenAPI(opCtx openapi.OperationContext ) } +func (handler *healthOpenAPIHandler) AuditDef() *pkghandler.AuditDef { + // Health endpoints are not audited since they don't represent user actions and are called frequently by monitoring systems, which would create noise in the audit logs. + return nil +} + func (provider *provider) addRegistryRoutes(router *mux.Router) error { if err := router.Handle("/api/v2/healthz", newHealthOpenAPIHandler( provider.authZ.OpenAccess(provider.factoryHandler.Healthz), diff --git a/pkg/auditor/auditorserver/server_test.go b/pkg/auditor/auditorserver/server_test.go index 5d9e3ae2472..4bd6cb1627f 100644 --- a/pkg/auditor/auditorserver/server_test.go +++ b/pkg/auditor/auditorserver/server_test.go @@ -21,11 +21,15 @@ func newTestSettings() factory.ScopedProviderSettings { func newTestEvent(resource string, action audittypes.Action) audittypes.AuditEvent { return audittypes.AuditEvent{ - Timestamp: time.Now(), - EventName: audittypes.NewEventName(resource, action), - ResourceName: resource, - Action: action, - Outcome: audittypes.OutcomeSuccess, + Timestamp: time.Now(), + EventName: audittypes.NewEventName(resource, action), + AuditAttributes: audittypes.AuditAttributes{ + Action: action, + Outcome: audittypes.OutcomeSuccess, + }, + ResourceAttributes: audittypes.ResourceAttributes{ + ResourceName: resource, + }, } } diff --git a/pkg/errors/code.go b/pkg/errors/code.go index bd4cbb1b55a..becee67fdc6 100644 --- a/pkg/errors/code.go +++ b/pkg/errors/code.go @@ -20,6 +20,12 @@ var ( CodeLicenseUnavailable = Code{"license_unavailable"} ) +var ( + // Used when reverse engineering an error from a response that doesn't have a code. + // This should never be used in the codebase, and if it is, it's a bug that should be fixed by using proper error handling and including error codes in responses. + CodeUnset = Code{"unset"} +) + var ( codeRegex = regexp.MustCompile(`^[a-z_]+$`) ) diff --git a/pkg/http/handler/handler.go b/pkg/http/handler/handler.go index cb4e83e720e..b4dd9a9ee4f 100644 --- a/pkg/http/handler/handler.go +++ b/pkg/http/handler/handler.go @@ -15,14 +15,16 @@ type ServeOpenAPIFunc func(openapi.OperationContext) type Handler interface { http.Handler ServeOpenAPI(openapi.OperationContext) + AuditDef() *AuditDef } type handler struct { handlerFunc http.HandlerFunc openAPIDef OpenAPIDef + auditDef *AuditDef } -func New(handlerFunc http.HandlerFunc, openAPIDef OpenAPIDef) Handler { +func New(handlerFunc http.HandlerFunc, openAPIDef OpenAPIDef, opts ...Option) Handler { // Remove duplicate error status codes openAPIDef.ErrorStatusCodes = slices.DeleteFunc(openAPIDef.ErrorStatusCodes, func(statusCode int) bool { return statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden || statusCode == http.StatusInternalServerError @@ -36,10 +38,16 @@ func New(handlerFunc http.HandlerFunc, openAPIDef OpenAPIDef) Handler { openAPIDef.ErrorStatusCodes = append(openAPIDef.ErrorStatusCodes, http.StatusUnauthorized, http.StatusForbidden) } - return &handler{ + handler := &handler{ handlerFunc: handlerFunc, openAPIDef: openAPIDef, } + + for _, opt := range opts { + opt(handler) + } + + return handler } func (handler *handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { @@ -120,5 +128,8 @@ func (handler *handler) ServeOpenAPI(opCtx openapi.OperationContext) { openapi.WithHTTPStatus(statusCode), ) } +} +func (handler *handler) AuditDef() *AuditDef { + return handler.auditDef } diff --git a/pkg/http/handler/option.go b/pkg/http/handler/option.go new file mode 100644 index 00000000000..9c19d0fbee3 --- /dev/null +++ b/pkg/http/handler/option.go @@ -0,0 +1,24 @@ +package handler + +import ( + "github.com/SigNoz/signoz/pkg/types/audittypes" +) + +// Option configures optional behaviour on a handler created by New. +type Option func(*handler) + +type AuditDef struct { + ResourceName string // AuthZ Typeable.Name() value, e.g. "dashboard", "user". + Action audittypes.Action // create, update, delete, login, etc. + Category audittypes.ActionCategory // access_control, configuration_change, etc. + ResourceIDParam string // Gorilla mux path param name for the resource ID. +} + +// WithAudit attaches an AuditDef to the handler. The actual audit event +// emission is handled by the middleware layer, which reads the AuditDef +// from the matched route's handler. +func WithAuditDef(def AuditDef) Option { + return func(h *handler) { + h.auditDef = &def + } +} diff --git a/pkg/http/middleware/audit.go b/pkg/http/middleware/audit.go new file mode 100644 index 00000000000..d8147f2f08d --- /dev/null +++ b/pkg/http/middleware/audit.go @@ -0,0 +1,169 @@ +package middleware + +import ( + "log/slog" + "net" + "net/http" + "time" + + "github.com/gorilla/mux" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" + "go.opentelemetry.io/otel/trace" + + "github.com/SigNoz/signoz/pkg/auditor" + "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/http/handler" + "github.com/SigNoz/signoz/pkg/http/render" + "github.com/SigNoz/signoz/pkg/types/audittypes" + "github.com/SigNoz/signoz/pkg/types/authtypes" +) + +const ( + logMessage = "::RECEIVED-REQUEST::" +) + +type Audit struct { + logger *slog.Logger + excludedRoutes map[string]struct{} + auditor auditor.Auditor +} + +func NewAudit(logger *slog.Logger, excludedRoutes []string, auditor auditor.Auditor) *Audit { + excludedRoutesMap := make(map[string]struct{}) + for _, route := range excludedRoutes { + excludedRoutesMap[route] = struct{}{} + } + + return &Audit{ + logger: logger.With(slog.String("pkg", pkgname)), + excludedRoutes: excludedRoutesMap, + auditor: auditor, + } +} + +func (middleware *Audit) Wrap(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + start := time.Now() + host, port, _ := net.SplitHostPort(req.Host) + path, err := mux.CurrentRoute(req).GetPathTemplate() + if err != nil { + path = req.URL.Path + } + + fields := []any{ + string(semconv.ClientAddressKey), req.RemoteAddr, + string(semconv.UserAgentOriginalKey), req.UserAgent(), + string(semconv.ServerAddressKey), host, + string(semconv.ServerPortKey), port, + string(semconv.HTTPRequestSizeKey), req.ContentLength, + string(semconv.HTTPRouteKey), path, + } + + responseBuffer := &byteBuffer{} + writer := newResponseCapture(rw, responseBuffer) + next.ServeHTTP(writer, req) + + statusCode, writeErr := writer.StatusCode(), writer.WriteError() + + // Logging or Audit: skip if the matched route is in the excluded list. This allows us to exclude noisy routes (e.g. health checks) from both logging and audit. + if _, ok := middleware.excludedRoutes[path]; ok { + return + } + + middleware.emitAuditEvent(req, writer, path) + + fields = append(fields, + string(semconv.HTTPResponseStatusCodeKey), statusCode, + string(semconv.HTTPServerRequestDurationName), time.Since(start), + ) + if writeErr != nil { + fields = append(fields, errors.Attr(writeErr)) + middleware.logger.ErrorContext(req.Context(), logMessage, fields...) + } else { + if responseBuffer.Len() != 0 { + fields = append(fields, "response.body", responseBuffer.String()) + } + + middleware.logger.InfoContext(req.Context(), logMessage, fields...) + } + }) +} + +func (middleware *Audit) emitAuditEvent(req *http.Request, writer responseCapture, routeTemplate string) { + if middleware.auditor == nil { + return + } + + def := auditDefFromRequest(req) + if def == nil { + return + } + + // extract claims + claims, _ := authtypes.ClaimsFromContext(req.Context()) + + // extract status code + statusCode := writer.StatusCode() + + // extract traces. + span := trace.SpanFromContext(req.Context()) + + // extract error details. + var errorType, errorCode string + if statusCode >= 400 { + errorType = render.ErrorTypeFromStatusCode(statusCode) + errorCode = render.ErrorCodeFromBody(writer.BodyBytes()) + } + + event := audittypes.NewAuditEventFromHTTPRequest( + req, + routeTemplate, + statusCode, + span.SpanContext().TraceID(), + span.SpanContext().SpanID(), + def.Action, + def.Category, + claims, + resourceIDFromRequest(req, def.ResourceIDParam), + def.ResourceName, + errorType, + errorCode, + ) + + middleware.auditor.Audit(req.Context(), event) +} + +func auditDefFromRequest(req *http.Request) *handler.AuditDef { + route := mux.CurrentRoute(req) + if route == nil { + return nil + } + + actualHandler := route.GetHandler() + if actualHandler == nil { + return nil + } + + // The type assertion is necessary because route.GetHandler() returns + // http.Handler, and not every http.Handler on the mux is a handler.Handler + // (e.g. middleware wrappers, raw http.HandlerFunc registrations). + provider, ok := actualHandler.(handler.Handler) + if !ok { + return nil + } + + return provider.AuditDef() +} + +func resourceIDFromRequest(req *http.Request, param string) string { + if param == "" { + return "" + } + + vars := mux.Vars(req) + if vars == nil { + return "" + } + + return vars[param] +} diff --git a/pkg/http/middleware/logging.go b/pkg/http/middleware/logging.go deleted file mode 100644 index 7c03165c221..00000000000 --- a/pkg/http/middleware/logging.go +++ /dev/null @@ -1,81 +0,0 @@ -package middleware - -import ( - "bytes" - "log/slog" - "net" - "net/http" - "time" - - "github.com/gorilla/mux" - semconv "go.opentelemetry.io/otel/semconv/v1.26.0" - - "github.com/SigNoz/signoz/pkg/errors" -) - -const ( - logMessage string = "::RECEIVED-REQUEST::" -) - -type Logging struct { - logger *slog.Logger - excludedRoutes map[string]struct{} -} - -func NewLogging(logger *slog.Logger, excludedRoutes []string) *Logging { - excludedRoutesMap := make(map[string]struct{}) - for _, route := range excludedRoutes { - excludedRoutesMap[route] = struct{}{} - } - - return &Logging{ - logger: logger.With(slog.String("pkg", pkgname)), - excludedRoutes: excludedRoutesMap, - } -} - -func (middleware *Logging) Wrap(next http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - start := time.Now() - host, port, _ := net.SplitHostPort(req.Host) - path, err := mux.CurrentRoute(req).GetPathTemplate() - if err != nil { - path = req.URL.Path - } - - fields := []any{ - string(semconv.ClientAddressKey), req.RemoteAddr, - string(semconv.UserAgentOriginalKey), req.UserAgent(), - string(semconv.ServerAddressKey), host, - string(semconv.ServerPortKey), port, - string(semconv.HTTPRequestSizeKey), req.ContentLength, - string(semconv.HTTPRouteKey), path, - } - - badResponseBuffer := new(bytes.Buffer) - writer := newBadResponseLoggingWriter(rw, badResponseBuffer) - next.ServeHTTP(writer, req) - - // if the path is in the excludedRoutes map, don't log - if _, ok := middleware.excludedRoutes[path]; ok { - return - } - - statusCode, err := writer.StatusCode(), writer.WriteError() - fields = append(fields, - string(semconv.HTTPResponseStatusCodeKey), statusCode, - string(semconv.HTTPServerRequestDurationName), time.Since(start), - ) - if err != nil { - fields = append(fields, errors.Attr(err)) - middleware.logger.ErrorContext(req.Context(), logMessage, fields...) - } else { - // when the status code is 400 or >=500, and the response body is not empty. - if badResponseBuffer.Len() != 0 { - fields = append(fields, "response.body", badResponseBuffer.String()) - } - - middleware.logger.InfoContext(req.Context(), logMessage, fields...) - } - }) -} diff --git a/pkg/http/middleware/response.go b/pkg/http/middleware/response.go index b161afb0f9a..21ff0bc8947 100644 --- a/pkg/http/middleware/response.go +++ b/pkg/http/middleware/response.go @@ -2,7 +2,6 @@ package middleware import ( "bufio" - "io" "net" "net/http" @@ -10,118 +9,156 @@ import ( ) const ( - maxResponseBodyInLogs = 4096 // At most 4k bytes from response bodies in our logs. + maxResponseBodyCapture int = 4096 // At most 4k bytes from response bodies. ) -type badResponseLoggingWriter interface { +// Wraps an http.ResponseWriter to capture the status code, +// write errors, and (for error responses) a bounded slice of the body. +type responseCapture interface { http.ResponseWriter - // Get the status code. + + // StatusCode returns the HTTP status code written to the response. StatusCode() int - // Get the error while writing. + + // WriteError returns the error (if any) from the downstream Write call. WriteError() error + + // BodyBytes returns the captured response body bytes. Only populated + // for error responses (status >= 400). + BodyBytes() []byte } -func newBadResponseLoggingWriter(rw http.ResponseWriter, buffer io.Writer) badResponseLoggingWriter { - b := nonFlushingBadResponseLoggingWriter{ +func newResponseCapture(rw http.ResponseWriter, buffer *byteBuffer) responseCapture { + b := nonFlushingResponseCapture{ rw: rw, buffer: buffer, - logBody: false, - bodyBytesLeft: maxResponseBodyInLogs, + captureBody: false, + bodyBytesLeft: maxResponseBodyCapture, statusCode: http.StatusOK, } if f, ok := rw.(http.Flusher); ok { - return &flushingBadResponseLoggingWriter{b, f} + return &flushingResponseCapture{nonFlushingResponseCapture: b, f: f} } return &b } -type nonFlushingBadResponseLoggingWriter struct { +// byteBuffer is a minimal write-only buffer used to capture response bodies. +type byteBuffer struct { + buf []byte +} + +func (b *byteBuffer) Write(p []byte) (int, error) { + b.buf = append(b.buf, p...) + return len(p), nil +} + +func (b *byteBuffer) WriteString(s string) (int, error) { + b.buf = append(b.buf, s...) + return len(s), nil +} + +func (b *byteBuffer) Bytes() []byte { + return b.buf +} + +func (b *byteBuffer) Len() int { + return len(b.buf) +} + +func (b *byteBuffer) String() string { + return string(b.buf) +} + +type nonFlushingResponseCapture struct { rw http.ResponseWriter - buffer io.Writer - logBody bool + buffer *byteBuffer + captureBody bool bodyBytesLeft int statusCode int - writeError error // The error returned when downstream Write() fails. + writeError error } -// Extends nonFlushingBadResponseLoggingWriter that implements http.Flusher. -type flushingBadResponseLoggingWriter struct { - nonFlushingBadResponseLoggingWriter +type flushingResponseCapture struct { + nonFlushingResponseCapture f http.Flusher } -// Unwrap method is used by http.ResponseController to get access to original http.ResponseWriter. -func (writer *nonFlushingBadResponseLoggingWriter) Unwrap() http.ResponseWriter { +// Unwrap is used by http.ResponseController to get access to original http.ResponseWriter. +func (writer *nonFlushingResponseCapture) Unwrap() http.ResponseWriter { return writer.rw } // Header returns the header map that will be sent by WriteHeader. -// Implements ResponseWriter. -func (writer *nonFlushingBadResponseLoggingWriter) Header() http.Header { +func (writer *nonFlushingResponseCapture) Header() http.Header { return writer.rw.Header() } // WriteHeader writes the HTTP response header. -func (writer *nonFlushingBadResponseLoggingWriter) WriteHeader(statusCode int) { +func (writer *nonFlushingResponseCapture) WriteHeader(statusCode int) { writer.statusCode = statusCode - if statusCode >= 500 || statusCode == 400 { - writer.logBody = true + if statusCode >= 400 { + writer.captureBody = true } + writer.rw.WriteHeader(statusCode) } -// Writes HTTP response data. -func (writer *nonFlushingBadResponseLoggingWriter) Write(data []byte) (int, error) { +// Write writes HTTP response data. +func (writer *nonFlushingResponseCapture) Write(data []byte) (int, error) { if writer.statusCode == 0 { - // WriteHeader has (probably) not been called, so we need to call it with StatusOK to fulfill the interface contract. - // https://godoc.org/net/http#ResponseWriter writer.WriteHeader(http.StatusOK) } - // 204 No Content is a success response that indicates that the request has been successfully processed and that the response body is intentionally empty. if writer.statusCode == 204 { return 0, nil } n, err := writer.rw.Write(data) - if writer.logBody { + if writer.captureBody { writer.captureResponseBody(data) } + if err != nil { writer.writeError = err } + return n, err } // Hijack hijacks the first response writer that is a Hijacker. -func (writer *nonFlushingBadResponseLoggingWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { +func (writer *nonFlushingResponseCapture) Hijack() (net.Conn, *bufio.ReadWriter, error) { hj, ok := writer.rw.(http.Hijacker) if ok { return hj.Hijack() } + return nil, nil, errors.NewInternalf(errors.CodeInternal, "cannot cast underlying response writer to Hijacker") } -func (writer *nonFlushingBadResponseLoggingWriter) StatusCode() int { +func (writer *nonFlushingResponseCapture) StatusCode() int { return writer.statusCode } -func (writer *nonFlushingBadResponseLoggingWriter) WriteError() error { +func (writer *nonFlushingResponseCapture) WriteError() error { return writer.writeError } -func (writer *flushingBadResponseLoggingWriter) Flush() { +func (writer *nonFlushingResponseCapture) BodyBytes() []byte { + return writer.buffer.Bytes() +} + +func (writer *flushingResponseCapture) Flush() { writer.f.Flush() } -func (writer *nonFlushingBadResponseLoggingWriter) captureResponseBody(data []byte) { +func (writer *nonFlushingResponseCapture) captureResponseBody(data []byte) { if len(data) > writer.bodyBytesLeft { _, _ = writer.buffer.Write(data[:writer.bodyBytesLeft]) - _, _ = io.WriteString(writer.buffer, "...") + _, _ = writer.buffer.WriteString("...") writer.bodyBytesLeft = 0 - writer.logBody = false + writer.captureBody = false } else { _, _ = writer.buffer.Write(data) writer.bodyBytesLeft -= len(data) diff --git a/pkg/http/middleware/response_test.go b/pkg/http/middleware/response_test.go new file mode 100644 index 00000000000..e431a3a9837 --- /dev/null +++ b/pkg/http/middleware/response_test.go @@ -0,0 +1,88 @@ +package middleware + +import ( + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestResponseCapture(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + handler http.HandlerFunc + expectedStatus int + expectedBodyBytes string + expectedClientBody string + }{ + { + name: "Success_DoesNotCaptureBody", + handler: func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusOK) + _, _ = rw.Write([]byte(`{"status":"success","data":{"id":"123"}}`)) + }, + expectedStatus: http.StatusOK, + expectedBodyBytes: "", + expectedClientBody: `{"status":"success","data":{"id":"123"}}`, + }, + { + name: "Error_CapturesBody", + handler: func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusForbidden) + _, _ = rw.Write([]byte(`{"status":"error","error":{"code":"authz_forbidden","message":"forbidden"}}`)) + }, + expectedStatus: http.StatusForbidden, + expectedBodyBytes: `{"status":"error","error":{"code":"authz_forbidden","message":"forbidden"}}`, + expectedClientBody: `{"status":"error","error":{"code":"authz_forbidden","message":"forbidden"}}`, + }, + { + name: "Error_TruncatesAtMaxCapture", + handler: func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusInternalServerError) + _, _ = rw.Write([]byte(strings.Repeat("x", maxResponseBodyCapture+100))) + }, + expectedStatus: http.StatusInternalServerError, + expectedBodyBytes: strings.Repeat("x", maxResponseBodyCapture) + "...", + expectedClientBody: strings.Repeat("x", maxResponseBodyCapture+100), + }, + { + name: "NoContent_SuppressesWrite", + handler: func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusNoContent) + _, _ = rw.Write([]byte("should be suppressed")) + }, + expectedStatus: http.StatusNoContent, + expectedBodyBytes: "", + expectedClientBody: "", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + var captured responseCapture + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + buf := &byteBuffer{} + captured = newResponseCapture(rw, buf) + testCase.handler(captured, req) + })) + defer server.Close() + + resp, err := http.Get(server.URL) + assert.NoError(t, err) + defer resp.Body.Close() + + clientBody, _ := io.ReadAll(resp.Body) + + assert.Equal(t, testCase.expectedStatus, captured.StatusCode()) + assert.Equal(t, testCase.expectedBodyBytes, string(captured.BodyBytes())) + assert.Equal(t, testCase.expectedClientBody, string(clientBody)) + }) + } +} diff --git a/pkg/http/render/render.go b/pkg/http/render/render.go index d7827e25671..771db847cef 100644 --- a/pkg/http/render/render.go +++ b/pkg/http/render/render.go @@ -5,6 +5,7 @@ import ( "github.com/SigNoz/signoz/pkg/errors" jsoniter "github.com/json-iterator/go" + "github.com/tidwall/gjson" ) const ( @@ -42,6 +43,45 @@ func Success(rw http.ResponseWriter, httpCode int, data interface{}) { _, _ = rw.Write(body) } +func ErrorCodeFromBody(body []byte) string { + code := gjson.GetBytes(body, "error.code").String() + + // This should never return empty since we only call this function on responses that were generated by us. + // If it does return empty, the codebase has failed to use render package for error responses somewhere, and we should fix that instead of trying to handle it here. + if code == "" { + return errors.CodeUnset.String() + } + + return code +} + +func ErrorTypeFromStatusCode(statusCode int) string { + // We are losing the exact type information here, but we can at least capture the error code and message for better observability. + // To get the exact type, we would need some changes in the render package to include the error type in the response, which we can consider in the future if there is a need for it. + switch statusCode { + case http.StatusBadRequest: + return errors.TypeInvalidInput.String() + case http.StatusNotFound: + return errors.TypeNotFound.String() + case http.StatusConflict: + return errors.TypeAlreadyExists.String() + case http.StatusUnauthorized: + return errors.TypeUnauthenticated.String() + case http.StatusNotImplemented: + return errors.TypeUnsupported.String() + case http.StatusForbidden: + return errors.TypeForbidden.String() + case statusClientClosedConnection: + return errors.TypeCanceled.String() + case http.StatusGatewayTimeout: + return errors.TypeTimeout.String() + case http.StatusUnavailableForLegalReasons: + return errors.TypeLicenseUnavailable.String() + default: + return errors.TypeInternal.String() + } +} + func Error(rw http.ResponseWriter, cause error) { // Derive the http code from the error type t, _, _, _, _, _ := errors.Unwrapb(cause) diff --git a/pkg/http/render/render_test.go b/pkg/http/render/render_test.go index 42f4565de7a..37d07a1416e 100644 --- a/pkg/http/render/render_test.go +++ b/pkg/http/render/render_test.go @@ -58,6 +58,31 @@ func TestSuccess(t *testing.T) { assert.Equal(t, expected, actual) } +func TestErrorCodeFromBody(t *testing.T) { + testCases := []struct { + name string + body []byte + wantCode string + }{ + { + name: "ValidErrorResponse", + body: []byte(`{"status":"error","error":{"code":"authz_forbidden","message":"only admins can access this resource"}}`), + wantCode: "authz_forbidden", + }, + { + name: "InvalidJSON", + body: []byte(`not json`), + wantCode: "unset", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + assert.Equal(t, testCase.wantCode, ErrorCodeFromBody(testCase.body)) + }) + } +} + func TestError(t *testing.T) { listener, err := net.Listen("tcp", "localhost:0") require.NoError(t, err) diff --git a/pkg/query-service/app/server.go b/pkg/query-service/app/server.go index b57d1ad9b2b..f4aada5fbe8 100644 --- a/pkg/query-service/app/server.go +++ b/pkg/query-service/app/server.go @@ -208,7 +208,7 @@ func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server, s.config.APIServer.Timeout.Default, s.config.APIServer.Timeout.Max, ).Wrap) - r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap) + r.Use(middleware.NewAudit(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes, nil).Wrap) r.Use(middleware.NewComment().Wrap) am := middleware.NewAuthZ(s.signoz.Instrumentation.Logger(), s.signoz.Modules.OrgGetter, s.signoz.Authz) diff --git a/pkg/types/audittypes/attributes.go b/pkg/types/audittypes/attributes.go new file mode 100644 index 00000000000..4462d0589e2 --- /dev/null +++ b/pkg/types/audittypes/attributes.go @@ -0,0 +1,206 @@ +package audittypes + +import ( + "net/http" + "strings" + + "github.com/SigNoz/signoz/pkg/types/authtypes" + "github.com/SigNoz/signoz/pkg/valuer" + "go.opentelemetry.io/collector/pdata/pcommon" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" +) + +// Audit attributes — Action (What). +type AuditAttributes struct { + Action Action // guaranteed to be present + ActionCategory ActionCategory // guaranteed to be present + Outcome Outcome // guaranteed to be present + IdentNProvider authtypes.IdentNProvider +} + +func NewAuditAttributesFromHTTP(statusCode int, action Action, category ActionCategory, claims authtypes.Claims) AuditAttributes { + outcome := OutcomeFailure + if statusCode >= 200 && statusCode < 400 { + outcome = OutcomeSuccess + } + + return AuditAttributes{ + Action: action, + ActionCategory: category, + Outcome: outcome, + IdentNProvider: claims.IdentNProvider, + } +} + +func (attributes AuditAttributes) Put(dest pcommon.Map) { + dest.PutStr("signoz.audit.action", attributes.Action.StringValue()) + dest.PutStr("signoz.audit.action_category", attributes.ActionCategory.StringValue()) + dest.PutStr("signoz.audit.outcome", attributes.Outcome.StringValue()) + putStrIfNotEmpty(dest, "signoz.audit.identn_provider", attributes.IdentNProvider.StringValue()) +} + +// Audit attributes — Principal (Who). +type PrincipalAttributes struct { + PrincipalType authtypes.Principal + PrincipalID valuer.UUID + PrincipalEmail valuer.Email + PrincipalOrgID valuer.UUID +} + +func NewPrincipalAttributesFromClaims(claims authtypes.Claims) PrincipalAttributes { + principalID, _ := valuer.NewUUID(claims.UserID) + principalEmail, _ := valuer.NewEmail(claims.Email) + principalOrgID, _ := valuer.NewUUID(claims.OrgID) + + return PrincipalAttributes{ + PrincipalType: claims.Principal, + PrincipalID: principalID, + PrincipalEmail: principalEmail, + PrincipalOrgID: principalOrgID, + } +} + +func (attributes PrincipalAttributes) Put(dest pcommon.Map) { + dest.PutStr("signoz.audit.principal.id", attributes.PrincipalID.StringValue()) + dest.PutStr("signoz.audit.principal.email", attributes.PrincipalEmail.String()) + dest.PutStr("signoz.audit.principal.type", attributes.PrincipalType.StringValue()) + dest.PutStr("signoz.audit.principal.org_id", attributes.PrincipalOrgID.StringValue()) +} + +// Audit attributes — Resource (On What). +type ResourceAttributes struct { + ResourceID string + ResourceName string // guaranteed to be present +} + +func NewResourceAttributes(resourceID, resourceName string) ResourceAttributes { + return ResourceAttributes{ + ResourceID: resourceID, + ResourceName: resourceName, + } +} + +func (attributes ResourceAttributes) Put(dest pcommon.Map) { + putStrIfNotEmpty(dest, "signoz.audit.resource.name", attributes.ResourceName) + putStrIfNotEmpty(dest, "signoz.audit.resource.id", attributes.ResourceID) +} + +// Audit attributes — Error (When outcome is failure) +// Error messages are intentionally excluded to avoid leaking sensitive or +// PII data into audit logs. The error type and code are sufficient for +// filtering and alerting; investigators can correlate via trace ID. +type ErrorAttributes struct { + ErrorType string + ErrorCode string +} + +func NewErrorAttributes(errorType, errorCode string) ErrorAttributes { + return ErrorAttributes{ + ErrorType: errorType, + ErrorCode: errorCode, + } +} + +func (attributes ErrorAttributes) Put(dest pcommon.Map) { + putStrIfNotEmpty(dest, "signoz.audit.error.type", attributes.ErrorType) + putStrIfNotEmpty(dest, "signoz.audit.error.code", attributes.ErrorCode) +} + +// Audit attributes — Transport Context (Where/How). +type TransportAttributes struct { + HTTPMethod string + HTTPRoute string + HTTPStatusCode int + URLPath string + ClientAddress string + UserAgent string +} + +func NewTransportAttributesFromHTTP(req *http.Request, route string, statusCode int) TransportAttributes { + return TransportAttributes{ + HTTPMethod: req.Method, + HTTPRoute: route, + HTTPStatusCode: statusCode, + URLPath: req.URL.Path, + ClientAddress: req.RemoteAddr, + UserAgent: req.UserAgent(), + } +} + +func (attributes TransportAttributes) Put(dest pcommon.Map) { + putStrIfNotEmpty(dest, string(semconv.HTTPRequestMethodKey), attributes.HTTPMethod) + putStrIfNotEmpty(dest, string(semconv.HTTPRouteKey), attributes.HTTPRoute) + if attributes.HTTPStatusCode != 0 { + dest.PutInt(string(semconv.HTTPResponseStatusCodeKey), int64(attributes.HTTPStatusCode)) + } + putStrIfNotEmpty(dest, string(semconv.URLPathKey), attributes.URLPath) + putStrIfNotEmpty(dest, string(semconv.ClientAddressKey), attributes.ClientAddress) + putStrIfNotEmpty(dest, string(semconv.UserAgentOriginalKey), attributes.UserAgent) +} + +func putStrIfNotEmpty(attrs pcommon.Map, key, value string) { + if value != "" { + attrs.PutStr(key, value) + } +} + +func newBody(auditAttributes AuditAttributes, principalAttributes PrincipalAttributes, resourceAttributes ResourceAttributes, errorAttributes ErrorAttributes) string { + var b strings.Builder + + // Principal: "email (id)" or "id" or "email" or omitted. + hasEmail := principalAttributes.PrincipalEmail.String() != "" + hasID := !principalAttributes.PrincipalID.IsZero() + if hasEmail { + b.WriteString(principalAttributes.PrincipalEmail.String()) + if hasID { + b.WriteString(" (") + b.WriteString(principalAttributes.PrincipalID.StringValue()) + b.WriteString(")") + } + } else if hasID { + b.WriteString(principalAttributes.PrincipalID.StringValue()) + } + + // Action: " created" or " failed to create". + if b.Len() > 0 { + b.WriteString(" ") + } + if auditAttributes.Outcome == OutcomeSuccess { + b.WriteString(auditAttributes.Action.PastTense()) + } else { + b.WriteString("failed to ") + b.WriteString(auditAttributes.Action.StringValue()) + } + + // Resource: " name (id)" or " name". + b.WriteString(" ") + b.WriteString(resourceAttributes.ResourceName) + if resourceAttributes.ResourceID != "" { + b.WriteString(" (") + b.WriteString(resourceAttributes.ResourceID) + b.WriteString(")") + } + + // Error suffix (failure only): ": type (code)" or ": type" or ": (code)" or omitted. + if auditAttributes.Outcome == OutcomeFailure { + errorType := errorAttributes.ErrorType + errorCode := errorAttributes.ErrorCode + if errorType != "" || errorCode != "" { + b.WriteString(": ") + if errorType != "" && errorCode != "" { + b.WriteString(errorType) + b.WriteString(" (") + b.WriteString(errorCode) + b.WriteString(")") + } else if errorType != "" { + b.WriteString(errorType) + } else { + b.WriteString("(") + b.WriteString(errorCode) + b.WriteString(")") + } + } + } + + return b.String() +} diff --git a/pkg/types/audittypes/attributes_test.go b/pkg/types/audittypes/attributes_test.go new file mode 100644 index 00000000000..26f6ee9ad87 --- /dev/null +++ b/pkg/types/audittypes/attributes_test.go @@ -0,0 +1,203 @@ +package audittypes + +import ( + "testing" + + "github.com/SigNoz/signoz/pkg/types/authtypes" + "github.com/SigNoz/signoz/pkg/valuer" + "github.com/stretchr/testify/assert" +) + +func TestNewAuditAttributesFromHTTP_OutcomeBoundary(t *testing.T) { + claims := authtypes.Claims{IdentNProvider: authtypes.IdentNProviderTokenizer} + + testCases := []struct { + name string + statusCode int + expectedOutcome Outcome + }{ + { + name: "200_Success", + statusCode: 200, + expectedOutcome: OutcomeSuccess, + }, + { + name: "399_Success", + statusCode: 399, + expectedOutcome: OutcomeSuccess, + }, + { + name: "400_Failure", + statusCode: 400, + expectedOutcome: OutcomeFailure, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + attrs := NewAuditAttributesFromHTTP(testCase.statusCode, ActionUpdate, ActionCategoryConfigurationChange, claims) + assert.Equal(t, testCase.expectedOutcome, attrs.Outcome) + }) + } +} + +func TestNewBody(t *testing.T) { + testCases := []struct { + name string + auditAttributes AuditAttributes + principalAttributes PrincipalAttributes + resourceAttributes ResourceAttributes + errorAttributes ErrorAttributes + expectedBody string + }{ + { + name: "Success_EmptyResourceID", + auditAttributes: AuditAttributes{ + Action: ActionDelete, + ActionCategory: ActionCategoryConfigurationChange, + Outcome: OutcomeSuccess, + }, + principalAttributes: PrincipalAttributes{ + PrincipalID: valuer.MustNewUUID("019a1234-abcd-7000-8000-567800000001"), + PrincipalEmail: valuer.MustNewEmail("test@acme.com"), + }, + resourceAttributes: ResourceAttributes{ + ResourceID: "", + ResourceName: "dashboard", + }, + errorAttributes: ErrorAttributes{}, + expectedBody: "test@acme.com (019a1234-abcd-7000-8000-567800000001) deleted dashboard", + }, + { + name: "Success_EmptyPrincipalEmail", + auditAttributes: AuditAttributes{ + Action: ActionDelete, + ActionCategory: ActionCategoryConfigurationChange, + Outcome: OutcomeSuccess, + }, + principalAttributes: PrincipalAttributes{ + PrincipalID: valuer.MustNewUUID("019a1234-abcd-7000-8000-567800000001"), + PrincipalEmail: valuer.Email{}, + }, + resourceAttributes: ResourceAttributes{ + ResourceID: "abd", + ResourceName: "dashboard", + }, + errorAttributes: ErrorAttributes{}, + expectedBody: "019a1234-abcd-7000-8000-567800000001 deleted dashboard (abd)", + }, + { + name: "Success_EmptyPrincipalIDandEmail", + auditAttributes: AuditAttributes{ + Action: ActionDelete, + ActionCategory: ActionCategoryConfigurationChange, + Outcome: OutcomeSuccess, + }, + principalAttributes: PrincipalAttributes{ + PrincipalID: valuer.UUID{}, + PrincipalEmail: valuer.Email{}, + }, + resourceAttributes: ResourceAttributes{ + ResourceID: "abd", + ResourceName: "dashboard", + }, + errorAttributes: ErrorAttributes{}, + expectedBody: "deleted dashboard (abd)", + }, + { + name: "Success_AllPresent", + auditAttributes: AuditAttributes{ + Action: ActionCreate, + ActionCategory: ActionCategoryConfigurationChange, + Outcome: OutcomeSuccess, + }, + principalAttributes: PrincipalAttributes{ + PrincipalID: valuer.MustNewUUID("019a1234-abcd-7000-8000-567800000001"), + PrincipalEmail: valuer.MustNewEmail("alice@acme.com"), + }, + resourceAttributes: ResourceAttributes{ + ResourceID: "019b-5678", + ResourceName: "dashboard", + }, + errorAttributes: ErrorAttributes{}, + expectedBody: "alice@acme.com (019a1234-abcd-7000-8000-567800000001) created dashboard (019b-5678)", + }, + { + name: "Success_EmptyEverythingOptional", + auditAttributes: AuditAttributes{ + Action: ActionUpdate, + ActionCategory: ActionCategoryConfigurationChange, + Outcome: OutcomeSuccess, + }, + principalAttributes: PrincipalAttributes{}, + resourceAttributes: ResourceAttributes{ + ResourceName: "alert-rule", + }, + errorAttributes: ErrorAttributes{}, + expectedBody: "updated alert-rule", + }, + { + name: "Failure_AllPresent", + auditAttributes: AuditAttributes{ + Action: ActionUpdate, + ActionCategory: ActionCategoryConfigurationChange, + Outcome: OutcomeFailure, + }, + principalAttributes: PrincipalAttributes{ + PrincipalID: valuer.MustNewUUID("019aaaaa-bbbb-7000-8000-cccc00000002"), + PrincipalEmail: valuer.MustNewEmail("viewer@acme.com"), + }, + resourceAttributes: ResourceAttributes{ + ResourceID: "019b-5678", + ResourceName: "dashboard", + }, + errorAttributes: ErrorAttributes{ + ErrorType: "forbidden", + ErrorCode: "authz_forbidden", + }, + expectedBody: "viewer@acme.com (019aaaaa-bbbb-7000-8000-cccc00000002) failed to update dashboard (019b-5678): forbidden (authz_forbidden)", + }, + { + name: "Failure_ErrorTypeOnly", + auditAttributes: AuditAttributes{ + Action: ActionDelete, + Outcome: OutcomeFailure, + }, + principalAttributes: PrincipalAttributes{ + PrincipalID: valuer.MustNewUUID("019a1234-abcd-7000-8000-567800000001"), + PrincipalEmail: valuer.MustNewEmail("test@acme.com"), + }, + resourceAttributes: ResourceAttributes{ + ResourceName: "user", + }, + errorAttributes: ErrorAttributes{ + ErrorType: "not-found", + }, + expectedBody: "test@acme.com (019a1234-abcd-7000-8000-567800000001) failed to delete user: not-found", + }, + { + name: "Failure_NoErrorDetails", + auditAttributes: AuditAttributes{ + Action: ActionCreate, + Outcome: OutcomeFailure, + }, + principalAttributes: PrincipalAttributes{ + PrincipalID: valuer.MustNewUUID("019a1234-abcd-7000-8000-567800000001"), + PrincipalEmail: valuer.MustNewEmail("test@acme.com"), + }, + resourceAttributes: ResourceAttributes{ + ResourceID: "019b-5678", + ResourceName: "dashboard", + }, + errorAttributes: ErrorAttributes{}, + expectedBody: "test@acme.com (019a1234-abcd-7000-8000-567800000001) failed to create dashboard (019b-5678)", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + body := newBody(testCase.auditAttributes, testCase.principalAttributes, testCase.resourceAttributes, testCase.errorAttributes) + assert.Equal(t, testCase.expectedBody, body) + }) + } +} diff --git a/pkg/types/audittypes/event.go b/pkg/types/audittypes/event.go index 09f861eeb66..19f3facbde5 100644 --- a/pkg/types/audittypes/event.go +++ b/pkg/types/audittypes/event.go @@ -1,54 +1,80 @@ package audittypes import ( - "encoding/hex" - "fmt" + "net/http" "time" - "github.com/SigNoz/signoz/pkg/valuer" + "github.com/SigNoz/signoz/pkg/types/authtypes" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/plog" - semconv "go.opentelemetry.io/otel/semconv/v1.10.0" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" + oteltrace "go.opentelemetry.io/otel/trace" ) -// AuditEvent represents a single audit log event. -// Fields are ordered following the OTel LogRecord structure. type AuditEvent struct { - // OTel LogRecord intrinsic fields - Timestamp time.Time `json:"timestamp"` - TraceID string `json:"traceId,omitempty"` - SpanID string `json:"spanId,omitempty"` - Body string `json:"body"` - EventName EventName `json:"eventName"` - - // Audit attributes — Principal (Who) - PrincipalID valuer.UUID `json:"principalId"` - PrincipalEmail valuer.Email `json:"principalEmail"` - PrincipalType PrincipalType `json:"principalType"` - PrincipalOrgID valuer.UUID `json:"principalOrgId"` - IdentNProvider string `json:"identnProvider,omitempty"` - - // Audit attributes — Action (What) - Action Action `json:"action"` - ActionCategory ActionCategory `json:"actionCategory"` - Outcome Outcome `json:"outcome"` - - // Audit attributes — Resource (On What) - ResourceName string `json:"resourceName"` - ResourceID string `json:"resourceId,omitempty"` - - // Audit attributes — Error (When outcome is failure) - ErrorType string `json:"errorType,omitempty"` - ErrorCode string `json:"errorCode,omitempty"` - ErrorMessage string `json:"errorMessage,omitempty"` - - // Transport Context (Where/How) - HTTPMethod string `json:"httpMethod,omitempty"` - HTTPRoute string `json:"httpRoute,omitempty"` - HTTPStatusCode int `json:"httpStatusCode,omitempty"` - URLPath string `json:"urlPath,omitempty"` - ClientAddress string `json:"clientAddress,omitempty"` - UserAgent string `json:"userAgent,omitempty"` + // OTel LogRecord Intrinsic + Timestamp time.Time + + // OTel LogRecord Intrinsic + TraceID oteltrace.TraceID + + // OTel LogRecord Intrinsic + SpanID oteltrace.SpanID + + // OTel LogRecord Intrinsic + Body string + + // OTel LogRecord Intrinsic + EventName EventName + + // Custom Audit Attributes - Action + AuditAttributes AuditAttributes + + // Custom Audit Attributes - Principal + PrincipalAttributes PrincipalAttributes + + // Custom Audit Attributes - Resource + ResourceAttributes ResourceAttributes + + // Custom Audit Attributes - Error + ErrorAttributes ErrorAttributes + + // Custom Audit Attributes - Transport Context + TransportAttributes TransportAttributes +} + +func NewAuditEventFromHTTPRequest( + req *http.Request, + route string, + statusCode int, + traceID oteltrace.TraceID, + spanID oteltrace.SpanID, + action Action, + actionCategory ActionCategory, + claims authtypes.Claims, + resourceID string, + resourceName string, + errorType string, + errorCode string, +) AuditEvent { + auditAttributes := NewAuditAttributesFromHTTP(statusCode, action, actionCategory, claims) + principalAttributes := NewPrincipalAttributesFromClaims(claims) + resourceAttributes := NewResourceAttributes(resourceID, resourceName) + errorAttributes := NewErrorAttributes(errorType, errorCode) + transportAttributes := NewTransportAttributesFromHTTP(req, route, statusCode) + + return AuditEvent{ + Timestamp: time.Now(), + TraceID: traceID, + SpanID: spanID, + Body: newBody(auditAttributes, principalAttributes, resourceAttributes, errorAttributes), + EventName: NewEventName(resourceAttributes.ResourceName, auditAttributes.Action), + AuditAttributes: auditAttributes, + PrincipalAttributes: principalAttributes, + ResourceAttributes: resourceAttributes, + ErrorAttributes: errorAttributes, + TransportAttributes: transportAttributes, + } } func NewPLogsFromAuditEvents(events []AuditEvent, name string, version string, scope string) plog.Logs { @@ -68,88 +94,41 @@ func NewPLogsFromAuditEvents(events []AuditEvent, name string, version string, s } func (event AuditEvent) ToLogRecord(dest plog.LogRecord) { + // Set timestamps dest.SetTimestamp(pcommon.NewTimestampFromTime(event.Timestamp)) dest.SetObservedTimestamp(pcommon.NewTimestampFromTime(event.Timestamp)) - dest.Body().SetStr(event.setBody()) + + // Set body and event name + dest.Body().SetStr(event.Body) dest.SetEventName(event.EventName.String()) - dest.SetSeverityNumber(event.Outcome.Severity()) - dest.SetSeverityText(event.Outcome.SeverityText()) - if tid, ok := parseTraceID(event.TraceID); ok { - dest.SetTraceID(tid) + // Set severity based on outcome + dest.SetSeverityNumber(event.AuditAttributes.Outcome.Severity()) + dest.SetSeverityText(event.AuditAttributes.Outcome.SeverityText()) + + // Set trace and span IDs if present + if event.TraceID.IsValid() { + dest.SetTraceID(pcommon.TraceID(event.TraceID)) } - if sid, ok := parseSpanID(event.SpanID); ok { - dest.SetSpanID(sid) + + if event.SpanID.IsValid() { + dest.SetSpanID(pcommon.SpanID(event.SpanID)) } attrs := dest.Attributes() - // Principal attributes - attrs.PutStr("signoz.audit.principal.id", event.PrincipalID.StringValue()) - attrs.PutStr("signoz.audit.principal.email", event.PrincipalEmail.String()) - attrs.PutStr("signoz.audit.principal.type", event.PrincipalType.StringValue()) - attrs.PutStr("signoz.audit.principal.org_id", event.PrincipalOrgID.StringValue()) - putStrIfNotEmpty(attrs, "signoz.audit.identn_provider", event.IdentNProvider) + // Audit attributes + event.AuditAttributes.Put(attrs) - // Action attributes - attrs.PutStr("signoz.audit.action", event.Action.StringValue()) - attrs.PutStr("signoz.audit.action_category", event.ActionCategory.StringValue()) - attrs.PutStr("signoz.audit.outcome", event.Outcome.StringValue()) + // Principal attributes + event.PrincipalAttributes.Put(attrs) // Resource attributes - attrs.PutStr("signoz.audit.resource.name", event.ResourceName) - putStrIfNotEmpty(attrs, "signoz.audit.resource.id", event.ResourceID) + event.ResourceAttributes.Put(attrs) - // Error attributes (on failure) - putStrIfNotEmpty(attrs, "signoz.audit.error.type", event.ErrorType) - putStrIfNotEmpty(attrs, "signoz.audit.error.code", event.ErrorCode) - putStrIfNotEmpty(attrs, "signoz.audit.error.message", event.ErrorMessage) + // Error attributes + event.ErrorAttributes.Put(attrs) // Transport context attributes - putStrIfNotEmpty(attrs, "http.request.method", event.HTTPMethod) - putStrIfNotEmpty(attrs, "http.route", event.HTTPRoute) - if event.HTTPStatusCode != 0 { - attrs.PutInt("http.response.status_code", int64(event.HTTPStatusCode)) - } - putStrIfNotEmpty(attrs, "url.path", event.URLPath) - putStrIfNotEmpty(attrs, "client.address", event.ClientAddress) - putStrIfNotEmpty(attrs, "user_agent.original", event.UserAgent) -} - -func (event AuditEvent) setBody() string { - if event.Outcome == OutcomeSuccess { - return fmt.Sprintf("%s (%s) %s %s %s", event.PrincipalEmail, event.PrincipalID, event.Action.PastTense(), event.ResourceName, event.ResourceID) - } - - return fmt.Sprintf("%s (%s) failed to %s %s %s: %s (%s)", event.PrincipalEmail, event.PrincipalID, event.Action.StringValue(), event.ResourceName, event.ResourceID, event.ErrorType, event.ErrorCode) -} - -func putStrIfNotEmpty(attrs pcommon.Map, key, value string) { - if value != "" { - attrs.PutStr(key, value) - } -} - -func parseTraceID(s string) (pcommon.TraceID, bool) { - b, err := hex.DecodeString(s) - if err != nil || len(b) != 16 { - return pcommon.TraceID{}, false - } - - var tid pcommon.TraceID - copy(tid[:], b) - - return tid, true -} - -func parseSpanID(s string) (pcommon.SpanID, bool) { - b, err := hex.DecodeString(s) - if err != nil || len(b) != 8 { - return pcommon.SpanID{}, false - } - - var sid pcommon.SpanID - copy(sid[:], b) - - return sid, true + event.TransportAttributes.Put(attrs) } diff --git a/pkg/types/audittypes/event_test.go b/pkg/types/audittypes/event_test.go new file mode 100644 index 00000000000..8fdd6a8eb0e --- /dev/null +++ b/pkg/types/audittypes/event_test.go @@ -0,0 +1,99 @@ +package audittypes + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/SigNoz/signoz/pkg/types/authtypes" + "github.com/stretchr/testify/assert" + oteltrace "go.opentelemetry.io/otel/trace" +) + +func TestNewAuditEventFromHTTPRequest(t *testing.T) { + traceID := oteltrace.TraceID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} + spanID := oteltrace.SpanID{1, 2, 3, 4, 5, 6, 7, 8} + + testCases := []struct { + name string + method string + path string + route string + statusCode int + action Action + category ActionCategory + claims authtypes.Claims + resourceID string + resourceName string + errorType string + errorCode string + expectedOutcome Outcome + expectedBody string + }{ + { + name: "Success_DashboardCreated", + method: http.MethodPost, + path: "/api/v1/dashboards", + route: "/api/v1/dashboards", + statusCode: http.StatusOK, + action: ActionCreate, + category: ActionCategoryConfigurationChange, + claims: authtypes.Claims{UserID: "019a1234-abcd-7000-8000-567800000001", Email: "alice@acme.com", OrgID: "019a-0000-0000-0001", IdentNProvider: authtypes.IdentNProviderTokenizer}, + resourceID: "019b-5678-efgh-9012", + resourceName: "dashboard", + expectedOutcome: OutcomeSuccess, + expectedBody: "alice@acme.com (019a1234-abcd-7000-8000-567800000001) created dashboard (019b-5678-efgh-9012)", + }, + { + name: "Failure_ForbiddenDashboardUpdate", + method: http.MethodPut, + path: "/api/v1/dashboards/019b-5678-efgh-9012", + route: "/api/v1/dashboards/{id}", + statusCode: http.StatusForbidden, + action: ActionUpdate, + category: ActionCategoryConfigurationChange, + claims: authtypes.Claims{UserID: "019aaaaa-bbbb-7000-8000-cccc00000002", Email: "viewer@acme.com", OrgID: "019a-0000-0000-0001", IdentNProvider: authtypes.IdentNProviderTokenizer}, + resourceID: "019b-5678-efgh-9012", + resourceName: "dashboard", + errorType: "forbidden", + errorCode: "authz_forbidden", + expectedOutcome: OutcomeFailure, + expectedBody: "viewer@acme.com (019aaaaa-bbbb-7000-8000-cccc00000002) failed to update dashboard (019b-5678-efgh-9012): forbidden (authz_forbidden)", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + req := httptest.NewRequest(testCase.method, testCase.path, nil) + + event := NewAuditEventFromHTTPRequest( + req, + testCase.route, + testCase.statusCode, + traceID, + spanID, + testCase.action, + testCase.category, + testCase.claims, + testCase.resourceID, + testCase.resourceName, + testCase.errorType, + testCase.errorCode, + ) + + assert.Equal(t, testCase.expectedOutcome, event.AuditAttributes.Outcome) + assert.Equal(t, testCase.expectedBody, event.Body) + assert.Equal(t, testCase.resourceName, event.ResourceAttributes.ResourceName) + assert.Equal(t, testCase.resourceID, event.ResourceAttributes.ResourceID) + assert.Equal(t, testCase.action, event.AuditAttributes.Action) + assert.Equal(t, testCase.category, event.AuditAttributes.ActionCategory) + assert.Equal(t, testCase.route, event.TransportAttributes.HTTPRoute) + assert.Equal(t, testCase.statusCode, event.TransportAttributes.HTTPStatusCode) + assert.Equal(t, testCase.method, event.TransportAttributes.HTTPMethod) + assert.Equal(t, traceID, event.TraceID) + assert.Equal(t, spanID, event.SpanID) + assert.Equal(t, testCase.errorType, event.ErrorAttributes.ErrorType) + assert.Equal(t, testCase.errorCode, event.ErrorAttributes.ErrorCode) + }) + } +} From 23e3c75d245fadbfc11feb6938050c170c7bd45c Mon Sep 17 00:00:00 2001 From: Nikhil Soni Date: Wed, 1 Apr 2026 15:48:36 +0530 Subject: [PATCH 68/78] feat: return all spans for flamegraph under a limit (#10757) * feat: return all spans for flamegraph under a limit * feat: increase fg limits and add timestamp boundaries * fix: set default value for ts boundary * fix: use correct value for boundary end ts * chore: change info log of flamegraph to debug --- .../app/clickhouseReader/reader.go | 20 +++++++++-- .../app/traces/tracedetail/flamegraph.go | 35 +++++++++++++------ pkg/query-service/model/queryParams.go | 5 ++- pkg/query-service/model/response.go | 1 + .../utils/timestamp/timestamp.go | 4 +++ 5 files changed, 50 insertions(+), 15 deletions(-) diff --git a/pkg/query-service/app/clickhouseReader/reader.go b/pkg/query-service/app/clickhouseReader/reader.go index 5bbacaac729..f11a163226b 100644 --- a/pkg/query-service/app/clickhouseReader/reader.go +++ b/pkg/query-service/app/clickhouseReader/reader.go @@ -18,6 +18,7 @@ import ( "github.com/uptrace/bun" "github.com/SigNoz/signoz/pkg/prometheus" + "github.com/SigNoz/signoz/pkg/query-service/utils/timestamp" "github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/telemetrystore" "github.com/SigNoz/signoz/pkg/types" @@ -1257,7 +1258,7 @@ func (r *ClickHouseReader) GetFlamegraphSpansForTrace(ctx context.Context, orgID } } - selectedSpans = tracedetail.GetSelectedSpansForFlamegraph(traceRoots, spanIdToSpanNodeMap) + selectedSpans = tracedetail.GetAllSpansForFlamegraph(traceRoots, spanIdToSpanNodeMap) traceCache := model.GetFlamegraphSpansForTraceCache{ StartTime: startTime, EndTime: endTime, @@ -1274,12 +1275,25 @@ func (r *ClickHouseReader) GetFlamegraphSpansForTrace(ctx context.Context, orgID } processingPostCache := time.Now() - selectedSpansForRequest := tracedetail.GetSelectedSpansForFlamegraphForRequest(req.SelectedSpanID, selectedSpans, startTime, endTime) - r.logger.Info("getFlamegraphSpansForTrace: processing post cache", "duration", time.Since(processingPostCache), "traceID", traceID) + selectedSpansForRequest := selectedSpans + clientLimit := min(req.Limit, tracedetail.MaxLimitWithoutSampling) + totalSpanCount := tracedetail.GetTotalSpanCount(selectedSpans) + if totalSpanCount > uint64(clientLimit) { + // using trace start and end time if boundary ts are set to zero (or not set) + boundaryStart := max(timestamp.MilliToNano(req.BoundaryStartTS), startTime) + boundaryEnd := timestamp.MilliToNano(req.BoundaryEndTS) + if boundaryEnd == 0 { + boundaryEnd = endTime + } + + selectedSpansForRequest = tracedetail.GetSelectedSpansForFlamegraphForRequest(req.SelectedSpanID, selectedSpans, boundaryStart, boundaryEnd) + } + r.logger.Debug("getFlamegraphSpansForTrace: processing post cache", "duration", time.Since(processingPostCache), "traceID", traceID, "totalSpans", totalSpanCount, "limit", clientLimit) trace.Spans = selectedSpansForRequest trace.StartTimestampMillis = startTime / 1000000 trace.EndTimestampMillis = endTime / 1000000 + trace.HasMore = totalSpanCount > uint64(clientLimit) return trace, nil } diff --git a/pkg/query-service/app/traces/tracedetail/flamegraph.go b/pkg/query-service/app/traces/tracedetail/flamegraph.go index 02bfa91913a..745123364db 100644 --- a/pkg/query-service/app/traces/tracedetail/flamegraph.go +++ b/pkg/query-service/app/traces/tracedetail/flamegraph.go @@ -7,9 +7,12 @@ import ( ) var ( - SPAN_LIMIT_PER_REQUEST_FOR_FLAMEGRAPH float64 = 50 - SPAN_LIMIT_PER_LEVEL int = 100 - TIMESTAMP_SAMPLING_BUCKET_COUNT int = 50 + flamegraphSpanLevelLimit float64 = 50 + flamegraphSpanLimitPerLevel int = 100 + flamegraphSamplingBucketCount int = 50 + flamegraphTopLatencySpanCount int = 5 + + MaxLimitWithoutSampling uint = 120_000 ) func ContainsFlamegraphSpan(slice []*model.FlamegraphSpan, item *model.FlamegraphSpan) bool { @@ -52,7 +55,8 @@ func FindIndexForSelectedSpan(spans [][]*model.FlamegraphSpan, selectedSpanId st return selectedSpanLevel } -func GetSelectedSpansForFlamegraph(traceRoots []*model.FlamegraphSpan, spanIdToSpanNodeMap map[string]*model.FlamegraphSpan) [][]*model.FlamegraphSpan { +// GetAllSpansForFlamegraph groups all spans as per their level +func GetAllSpansForFlamegraph(traceRoots []*model.FlamegraphSpan, spanIdToSpanNodeMap map[string]*model.FlamegraphSpan) [][]*model.FlamegraphSpan { var traceIdLevelledFlamegraph = map[string]map[int64][]*model.FlamegraphSpan{} selectedSpans := [][]*model.FlamegraphSpan{} @@ -100,7 +104,7 @@ func getLatencyAndTimestampBucketedSpans(spans []*model.FlamegraphSpan, selected }) // pick the top 5 latency spans - for idx := range 5 { + for idx := range flamegraphTopLatencySpanCount { sampledSpans = append(sampledSpans, spans[idx]) } @@ -117,17 +121,17 @@ func getLatencyAndTimestampBucketedSpans(spans []*model.FlamegraphSpan, selected } } - bucketSize := (endTime - startTime) / uint64(TIMESTAMP_SAMPLING_BUCKET_COUNT) + bucketSize := (endTime - startTime) / uint64(flamegraphSamplingBucketCount) if bucketSize == 0 { bucketSize = 1 } - bucketedSpans := make([][]*model.FlamegraphSpan, 50) + bucketedSpans := make([][]*model.FlamegraphSpan, flamegraphSamplingBucketCount) for _, span := range spans { if span.TimeUnixNano >= startTime && span.TimeUnixNano <= endTime { bucketIndex := int((span.TimeUnixNano - startTime) / bucketSize) - if bucketIndex >= 0 && bucketIndex < 50 { + if bucketIndex >= 0 && bucketIndex < flamegraphSamplingBucketCount { bucketedSpans[bucketIndex] = append(bucketedSpans[bucketIndex], span) } } @@ -156,8 +160,8 @@ func GetSelectedSpansForFlamegraphForRequest(selectedSpanID string, selectedSpan selectedIndex = FindIndexForSelectedSpan(selectedSpans, selectedSpanID) } - lowerLimit := selectedIndex - int(SPAN_LIMIT_PER_REQUEST_FOR_FLAMEGRAPH*0.4) - upperLimit := selectedIndex + int(SPAN_LIMIT_PER_REQUEST_FOR_FLAMEGRAPH*0.6) + lowerLimit := selectedIndex - int(flamegraphSpanLevelLimit*0.4) + upperLimit := selectedIndex + int(flamegraphSpanLevelLimit*0.6) if lowerLimit < 0 { upperLimit = upperLimit - lowerLimit @@ -174,7 +178,7 @@ func GetSelectedSpansForFlamegraphForRequest(selectedSpanID string, selectedSpan } for i := lowerLimit; i < upperLimit; i++ { - if len(selectedSpans[i]) > SPAN_LIMIT_PER_LEVEL { + if len(selectedSpans[i]) > flamegraphSpanLimitPerLevel { _spans := getLatencyAndTimestampBucketedSpans(selectedSpans[i], selectedSpanID, i == selectedIndex, startTime, endTime) selectedSpansForRequest = append(selectedSpansForRequest, _spans) } else { @@ -184,3 +188,12 @@ func GetSelectedSpansForFlamegraphForRequest(selectedSpanID string, selectedSpan return selectedSpansForRequest } + +func GetTotalSpanCount(spans [][]*model.FlamegraphSpan) uint64 { + levelCount := len(spans) + spanCount := uint64(0) + for i := range levelCount { + spanCount += uint64(len(spans[i])) + } + return spanCount +} diff --git a/pkg/query-service/model/queryParams.go b/pkg/query-service/model/queryParams.go index b8a9059ec31..56faa8032f9 100644 --- a/pkg/query-service/model/queryParams.go +++ b/pkg/query-service/model/queryParams.go @@ -337,7 +337,10 @@ type GetWaterfallSpansForTraceWithMetadataParams struct { } type GetFlamegraphSpansForTraceParams struct { - SelectedSpanID string `json:"selectedSpanId"` + SelectedSpanID string `json:"selectedSpanId"` + Limit uint `json:"limit"` + BoundaryStartTS uint64 `json:"boundaryStartTsMilli"` + BoundaryEndTS uint64 `json:"boundarEndTsMilli"` } type SpanFilterParams struct { diff --git a/pkg/query-service/model/response.go b/pkg/query-service/model/response.go index ede438dc3d1..9e83bca7a12 100644 --- a/pkg/query-service/model/response.go +++ b/pkg/query-service/model/response.go @@ -337,6 +337,7 @@ type GetFlamegraphSpansForTraceResponse struct { EndTimestampMillis uint64 `json:"endTimestampMillis"` DurationNano uint64 `json:"durationNano"` Spans [][]*FlamegraphSpan `json:"spans"` + HasMore bool `json:"hasMore"` } type OtelSpanRef struct { diff --git a/pkg/query-service/utils/timestamp/timestamp.go b/pkg/query-service/utils/timestamp/timestamp.go index 6bc2b64ee81..71e8c6829d4 100644 --- a/pkg/query-service/utils/timestamp/timestamp.go +++ b/pkg/query-service/utils/timestamp/timestamp.go @@ -11,3 +11,7 @@ func FromTime(t time.Time) int64 { func Time(ts int64) time.Time { return time.Unix(ts/1000, (ts%1000)*int64(time.Millisecond)) } + +func MilliToNano(milliTS uint64) uint64 { + return milliTS * 1000_000 +} From 10805539056e859bd125541c68796a9df9f96e23 Mon Sep 17 00:00:00 2001 From: Liapis Nikolaos <57102404+liapisn@users.noreply.github.com> Date: Wed, 1 Apr 2026 16:01:09 +0300 Subject: [PATCH 69/78] feat(logs): pretty-print JSON attribute values when copying to clipboard (#10778) When copying log attribute values that contain valid JSON objects or arrays, the value is now pretty-printed with 2-space indentation. This makes it easy to paste into JSON tools or editors. Non-JSON values (strings, numbers, booleans) are unaffected. Closes #8208 --- .../LogDetailedView/TableView/TableViewActions.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx b/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx index 98dac63682d..5d277ac7d6b 100644 --- a/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx +++ b/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx @@ -284,6 +284,15 @@ export default function TableViewActions( error, ); } + // If the value is valid JSON (object or array), pretty-print it for copying + try { + const parsed = JSON.parse(text); + if (typeof parsed === 'object' && parsed !== null) { + return JSON.stringify(parsed, null, 2); + } + } catch { + // not JSON, return as-is + } return text; }, [fieldData.value]); From 3dc0a7c8ce6cef6190072d238ac5b233c3cc9622 Mon Sep 17 00:00:00 2001 From: swapnil-signoz Date: Wed, 1 Apr 2026 19:38:46 +0530 Subject: [PATCH 70/78] feat: adding aws service definitions in types (#10798) * feat: adding aws service definitions in types * refactor: moving definitions to module fs --- .../implcloudintegration/definitionstore.go | 21 + .../aws/alb/assets/dashboards/overview.json | 1887 ++++++++++++ .../aws/alb/assets/dashboards/overview.png | Bin 0 -> 128236 bytes .../alb/assets/dashboards/overview_dot.json | 1887 ++++++++++++ .../fs/definitions/aws/alb/icon.svg | 1 + .../fs/definitions/aws/alb/integration.json | 468 +++ .../fs/definitions/aws/alb/overview.md | 3 + .../assets/dashboards/overview.json | 2269 ++++++++++++++ .../assets/dashboards/overview.png | Bin 0 -> 202518 bytes .../assets/dashboards/overview_dot.json | 2286 ++++++++++++++ .../fs/definitions/aws/api-gateway/icon.svg | 14 + .../aws/api-gateway/integration.json | 200 ++ .../definitions/aws/api-gateway/overview.md | 3 + .../dynamodb/assets/dashboards/overview.json | 2657 ++++++++++++++++ .../dynamodb/assets/dashboards/overview.png | Bin 0 -> 253551 bytes .../assets/dashboards/overview_dot.json | 2657 ++++++++++++++++ .../fs/definitions/aws/dynamodb/icon.svg | 18 + .../definitions/aws/dynamodb/integration.json | 395 +++ .../fs/definitions/aws/dynamodb/overview.md | 3 + .../aws/ec2/assets/dashboards/overview.json | 1446 +++++++++ .../aws/ec2/assets/dashboards/overview.png | Bin 0 -> 96334 bytes .../ec2/assets/dashboards/overview_dot.json | 1446 +++++++++ .../fs/definitions/aws/ec2/icon.svg | 11 + .../fs/definitions/aws/ec2/integration.json | 519 ++++ .../fs/definitions/aws/ec2/overview.md | 3 + .../assets/dashboards/containerinsights.json | 1965 ++++++++++++ .../assets/dashboards/containerinsights.png | Bin 0 -> 401276 bytes .../dashboards/containerinsights_dot.json | 1965 ++++++++++++ .../enhanced_containerinsights.json | 1576 ++++++++++ .../dashboards/enhanced_containerinsights.png | Bin 0 -> 133821 bytes .../enhanced_containerinsights_dot.json | 1576 ++++++++++ .../aws/ecs/assets/dashboards/overview.json | 851 +++++ .../aws/ecs/assets/dashboards/overview.png | Bin 0 -> 358591 bytes .../ecs/assets/dashboards/overview_dot.json | 851 +++++ .../fs/definitions/aws/ecs/icon.svg | 18 + .../fs/definitions/aws/ecs/integration.json | 871 ++++++ .../fs/definitions/aws/ecs/overview.md | 3 + .../assets/dashboards/containerinsights.json | 2182 +++++++++++++ .../assets/dashboards/containerinsights.png | Bin 0 -> 396690 bytes .../dashboards/containerinsights_dot.json | 2182 +++++++++++++ .../aws/eks/assets/dashboards/overview.json | 1748 +++++++++++ .../aws/eks/assets/dashboards/overview.png | Bin 0 -> 428884 bytes .../eks/assets/dashboards/overview_dot.json | 1748 +++++++++++ .../fs/definitions/aws/eks/icon.svg | 18 + .../fs/definitions/aws/eks/integration.json | 2744 +++++++++++++++++ .../fs/definitions/aws/eks/overview.md | 3 + .../assets/dashboards/redis_overview.json | 2706 ++++++++++++++++ .../assets/dashboards/redis_overview.png | Bin 0 -> 210435 bytes .../assets/dashboards/redis_overview_dot.json | 2706 ++++++++++++++++ .../fs/definitions/aws/elasticache/icon.svg | 18 + .../aws/elasticache/integration.json | 1955 ++++++++++++ .../definitions/aws/elasticache/overview.md | 3 + .../lambda/assets/dashboards/overview.json | 1547 ++++++++++ .../aws/lambda/assets/dashboards/overview.png | Bin 0 -> 134418 bytes .../assets/dashboards/overview_dot.json | 1547 ++++++++++ .../fs/definitions/aws/lambda/icon.svg | 1 + .../definitions/aws/lambda/integration.json | 300 ++ .../fs/definitions/aws/lambda/overview.md | 3 + .../aws/msk/assets/dashboards/overview.json | 1189 +++++++ .../aws/msk/assets/dashboards/overview.png | Bin 0 -> 379664 bytes .../msk/assets/dashboards/overview_dot.json | 1189 +++++++ .../fs/definitions/aws/msk/icon.svg | 1 + .../fs/definitions/aws/msk/integration.json | 1091 +++++++ .../fs/definitions/aws/msk/overview.md | 3 + .../aws/rds/assets/dashboards/overview.json | 1945 ++++++++++++ .../aws/rds/assets/dashboards/overview.png | Bin 0 -> 64743 bytes .../rds/assets/dashboards/overview_dot.json | 1945 ++++++++++++ .../fs/definitions/aws/rds/icon.svg | 21 + .../fs/definitions/aws/rds/integration.json | 804 +++++ .../fs/definitions/aws/rds/overview.md | 3 + .../fs/definitions/aws/s3sync/icon.svg | 1 + .../definitions/aws/s3sync/integration.json | 54 + .../fs/definitions/aws/s3sync/overview.md | 3 + .../aws/sns/assets/dashboards/overview.json | 818 +++++ .../aws/sns/assets/dashboards/overview.png | Bin 0 -> 288129 bytes .../sns/assets/dashboards/overview_dot.json | 818 +++++ .../fs/definitions/aws/sns/icon.svg | 18 + .../fs/definitions/aws/sns/integration.json | 131 + .../fs/definitions/aws/sns/overview.md | 3 + .../aws/sqs/assets/dashboards/overview.json | 1761 +++++++++++ .../aws/sqs/assets/dashboards/overview.png | Bin 0 -> 316643 bytes .../sqs/assets/dashboards/overview_dot.json | 1761 +++++++++++ .../fs/definitions/aws/sqs/icon.svg | 18 + .../fs/definitions/aws/sqs/integration.json | 251 ++ .../fs/definitions/aws/sqs/overview.md | 3 + pkg/types/cloudintegrationtypes/store.go | 5 + 86 files changed, 63117 insertions(+) create mode 100644 pkg/modules/cloudintegration/implcloudintegration/definitionstore.go create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/alb/assets/dashboards/overview.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/alb/assets/dashboards/overview.png create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/alb/assets/dashboards/overview_dot.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/alb/icon.svg create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/alb/integration.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/alb/overview.md create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/api-gateway/assets/dashboards/overview.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/api-gateway/assets/dashboards/overview.png create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/api-gateway/assets/dashboards/overview_dot.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/api-gateway/icon.svg create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/api-gateway/integration.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/api-gateway/overview.md create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/dynamodb/assets/dashboards/overview.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/dynamodb/assets/dashboards/overview.png create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/dynamodb/assets/dashboards/overview_dot.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/dynamodb/icon.svg create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/dynamodb/integration.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/dynamodb/overview.md create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ec2/assets/dashboards/overview.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ec2/assets/dashboards/overview.png create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ec2/assets/dashboards/overview_dot.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ec2/icon.svg create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ec2/integration.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ec2/overview.md create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ecs/assets/dashboards/containerinsights.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ecs/assets/dashboards/containerinsights.png create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ecs/assets/dashboards/containerinsights_dot.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ecs/assets/dashboards/enhanced_containerinsights.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ecs/assets/dashboards/enhanced_containerinsights.png create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ecs/assets/dashboards/enhanced_containerinsights_dot.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ecs/assets/dashboards/overview.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ecs/assets/dashboards/overview.png create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ecs/assets/dashboards/overview_dot.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ecs/icon.svg create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ecs/integration.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ecs/overview.md create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/eks/assets/dashboards/containerinsights.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/eks/assets/dashboards/containerinsights.png create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/eks/assets/dashboards/containerinsights_dot.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/eks/assets/dashboards/overview.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/eks/assets/dashboards/overview.png create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/eks/assets/dashboards/overview_dot.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/eks/icon.svg create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/eks/integration.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/eks/overview.md create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/elasticache/assets/dashboards/redis_overview.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/elasticache/assets/dashboards/redis_overview.png create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/elasticache/assets/dashboards/redis_overview_dot.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/elasticache/icon.svg create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/elasticache/integration.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/elasticache/overview.md create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/lambda/assets/dashboards/overview.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/lambda/assets/dashboards/overview.png create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/lambda/assets/dashboards/overview_dot.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/lambda/icon.svg create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/lambda/integration.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/lambda/overview.md create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/msk/assets/dashboards/overview.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/msk/assets/dashboards/overview.png create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/msk/assets/dashboards/overview_dot.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/msk/icon.svg create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/msk/integration.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/msk/overview.md create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/rds/assets/dashboards/overview.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/rds/assets/dashboards/overview.png create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/rds/assets/dashboards/overview_dot.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/rds/icon.svg create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/rds/integration.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/rds/overview.md create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/s3sync/icon.svg create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/s3sync/integration.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/s3sync/overview.md create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/sns/assets/dashboards/overview.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/sns/assets/dashboards/overview.png create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/sns/assets/dashboards/overview_dot.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/sns/icon.svg create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/sns/integration.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/sns/overview.md create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/sqs/assets/dashboards/overview.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/sqs/assets/dashboards/overview.png create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/sqs/assets/dashboards/overview_dot.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/sqs/icon.svg create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/sqs/integration.json create mode 100644 pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/sqs/overview.md diff --git a/pkg/modules/cloudintegration/implcloudintegration/definitionstore.go b/pkg/modules/cloudintegration/implcloudintegration/definitionstore.go new file mode 100644 index 00000000000..c71781eaf5f --- /dev/null +++ b/pkg/modules/cloudintegration/implcloudintegration/definitionstore.go @@ -0,0 +1,21 @@ +package implcloudintegration + +import ( + "context" + + citypes "github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes" +) + +type definitionStore struct{} + +func NewDefinitionStore() citypes.ServiceDefinitionStore { + return &definitionStore{} +} + +func (d *definitionStore) Get(ctx context.Context, provider citypes.CloudProviderType, serviceID citypes.ServiceID) (*citypes.ServiceDefinition, error) { + panic("unimplemented") +} + +func (d *definitionStore) List(ctx context.Context, provider citypes.CloudProviderType) ([]*citypes.ServiceDefinition, error) { + panic("unimplemented") +} diff --git a/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/alb/assets/dashboards/overview.json b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/alb/assets/dashboards/overview.json new file mode 100644 index 00000000000..38c60cba792 --- /dev/null +++ b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/alb/assets/dashboards/overview.json @@ -0,0 +1,1887 @@ +{ + "description": "Overview of Application Load Balancers", + "image": "data:image/svg+xml;base64,CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmlld0JveD0iMCAwIDg1IDg1IiBmaWxsPSIjZmZmIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48dXNlIHhsaW5rOmhyZWY9IiNBIiB4PSIyLjUiIHk9IjIuNSIvPjxzeW1ib2wgaWQ9IkEiIG92ZXJmbG93PSJ2aXNpYmxlIj48ZyBzdHJva2U9Im5vbmUiPjxwYXRoIGQ9Ik0wIDQxLjU3OUMwIDIwLjI5MyAxNy44NCAzLjE1NyA0MCAzLjE1N3M0MCAxNy4xMzYgNDAgMzguNDIyUzYyLjE2IDgwIDQwIDgwIDAgNjIuODY0IDAgNDEuNTc5eiIgZmlsbD0iIzlkNTAyNSIvPjxwYXRoIGQ9Ik0wIDM4LjQyMkMwIDE3LjEzNiAxNy44NCAwIDQwIDBzNDAgMTcuMTM2IDQwIDM4LjQyMi0xNy44NCAzOC40MjItNDAgMzguNDIyUzAgNTkuNzA3IDAgMzguNDIyeiIgZmlsbD0iI2Y1ODUzNiIvPjxwYXRoIGQ9Ik01MS42NzIgNy4zODd2MTMuOTUySDI4LjMyN1Y3LjM4N3ptMTguMDYxIDQwLjM3OHYxMS4zNjRoLTExLjgzVjQ3Ljc2NXptLTE0Ljk1OCAwdjExLjM2NGgtMTEuODNWNDcuNzY1em0tMTguMjA2IDB2MTEuMzY0aC0xMS44M1Y0Ny43NjV6bS0xNC45NTkgMHYxMS4zNjRIOS43OFY0Ny43NjV6Ii8+PHBhdGggZD0iTTE0LjYzIDM3LjkyOWgyLjEzdjExLjE0OWgtMi4xM3oiLz48cGF0aCBkPSJNMTQuNjMgMzcuOTI5aDE3LjA4OHYyLjA0NUgxNC42M3oiLz48cGF0aCBkPSJNMjkuNTg5IDM3LjkyOWgyLjEzdjExLjE0OUgyOS41OXptMTguMjA2IDBoMi4xM3YxMS4xNDloLTIuMTN6Ii8+PHBhdGggZD0iTTQ3Ljc5NSAzNy45MjloMTcuMDg4djIuMDQ1SDQ3Ljc5NXoiLz48cGF0aCBkPSJNNjIuNzU0IDM3LjkyOWgyLjEzdjExLjE0OWgtMi4xMjl6bS00MC42MzEtNy45NTRoMi4xM3Y4Ljk3N2gtMi4xM3pNMzguOTM1IDE5LjI4aDIuMTN2MTAuODU5aC0yLjEyOXoiLz48cGF0aCBkPSJNMjIuMTIzIDI5LjExNmgzNS4zMnYyLjA0NWgtMzUuMzJ6Ii8+PHBhdGggZD0iTTU1LjMxNCAyOS45NzVoMi4xM3Y4Ljk3N2gtMi4xMjl6Ii8+PC9nPjwvc3ltYm9sPjwvc3ZnPg==", + "layout": [ + { + "h": 5, + "i": "e13232f0-6308-4466-94c0-629cae762ff0", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 0 + }, + { + "h": 5, + "i": "9b99d70a-f12a-4df7-9a68-660a0ab55e42", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 0 + }, + { + "h": 5, + "i": "5a9ec75f-3bcd-4829-94e6-452caa2cc0d2", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 5 + }, + { + "h": 5, + "i": "e16fb999-491b-4cfa-b9aa-d558f2132b6b", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 5 + }, + { + "h": 5, + "i": "2be35406-693a-435b-a844-2df239be0b60", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 10 + }, + { + "h": 5, + "i": "480ecee2-1271-4dfd-a7bb-9f9845957c6e", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 10 + }, + { + "h": 5, + "i": "2243e542-0bbc-4e2a-a4dd-2e121abc9b95", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 15 + }, + { + "h": 5, + "i": "3bb56361-9a67-47ce-b186-ccee02e15f51", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 15 + }, + { + "h": 5, + "i": "36cbc321-6c02-4d13-895e-955d71d376b4", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 20 + } + ], + "panelMap": {}, + "tags": [], + "title": "Application Load Balancer Overview", + "uploadedGrafana": false, + "variables": { + "Account": { + "allSelected": false, + "customValue": "", + "description": "AWS Account", + "id": "d5ef5880-68b1-4097-b4e5-9ce74200831f", + "key": "d5ef5880-68b1-4097-b4e5-9ce74200831f", + "modificationUUID": "9974ddda-3bc1-401d-b04a-364ea9a23866", + "multiSelect": false, + "name": "Account", + "order": 0, + "queryValue": "SELECT JSONExtractString(labels, 'cloud_account_id') as cloud_account_id\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like 'aws_ApplicationELB_ConsumedLCUs_max'\nGROUP BY cloud_account_id", + "showALLOption": false, + "sort": "DISABLED", + "textboxValue": "", + "type": "QUERY" + }, + "Region": { + "allSelected": false, + "customValue": "", + "description": "AWS Region", + "id": "bad33abd-ab38-493b-b23e-131659b6d03c", + "key": "bad33abd-ab38-493b-b23e-131659b6d03c", + "modificationUUID": "1aa1ef37-260c-4ca9-8aa9-f2ffb289c9e3", + "multiSelect": false, + "name": "Region", + "order": 1, + "queryValue": "SELECT JSONExtractString(labels, 'cloud_region') as cloud_region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like 'aws_ApplicationELB_ConsumedLCUs_max'\n and JSONExtractString(labels, 'cloud_account_id') IN {{.Account}}\nGROUP BY cloud_region\n", + "showALLOption": false, + "sort": "DISABLED", + "textboxValue": "", + "type": "QUERY" + } + }, + "version": "v4", + "widgets": [ + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The time elapsed, after the request leaves the load balancer until the target starts to send the response headers.\n\nSee TargetResponseTime at https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-cloudwatch-metrics.html", + "fillSpans": false, + "id": "9b99d70a-f12a-4df7-9a68-660a0ab55e42", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApplicationELB_TargetResponseTime_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApplicationELB_TargetResponseTime_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "b282d9f1", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "71837c70", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "5bfcc581", + "key": { + "dataType": "string", + "id": "TargetGroup--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "TargetGroup", + "type": "tag" + }, + "op": "nexists", + "value": "" + }, + { + "id": "a9e33e08", + "key": { + "dataType": "string", + "id": "AvailabilityZone--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "AvailabilityZone", + "type": "tag" + }, + "op": "nexists", + "value": "" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "LoadBalancer--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "LoadBalancer", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + } + ], + "having": [], + "legend": "{{LoadBalancer}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "601aca8a-36fb-4e6a-b234-39294b9b305b", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Target Response Time", + "yAxisUnit": "s" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The number of requests processed over IPv4 and IPv6. This metric is only incremented for requests where the load balancer node was able to choose a target. Requests that are rejected before a target is chosen are not reflected in this metric.\n\nSee RequestCount at https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-cloudwatch-metrics.html", + "fillSpans": false, + "id": "e13232f0-6308-4466-94c0-629cae762ff0", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApplicationELB_RequestCount_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApplicationELB_RequestCount_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "448b551a", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "a8821216", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "f5a62c5a", + "key": { + "dataType": "string", + "id": "AvailabilityZone--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "AvailabilityZone", + "type": "tag" + }, + "op": "nexists", + "value": "" + }, + { + "id": "25e8abc8", + "key": { + "dataType": "string", + "id": "TargetGroup--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "TargetGroup", + "type": "tag" + }, + "op": "nexists", + "value": "" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "LoadBalancer--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "LoadBalancer", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + } + ], + "having": [], + "legend": "{{LoadBalancer}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "stepInterval": 60, + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "7d92a02f-3202-4059-8b88-2d24112a35e6", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Requests", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The number of HTTP 5XX response codes generated by the targets. This does not include any response codes generated by the load balancer.\n\nSee HTTPCode_Target_5XX_Count at https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-cloudwatch-metrics.html", + "fillSpans": false, + "id": "5a9ec75f-3bcd-4829-94e6-452caa2cc0d2", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApplicationELB_HTTPCode_Target_5XX_Count_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApplicationELB_HTTPCode_Target_5XX_Count_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "702a8765", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "32985f2d", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "e4cf3d8b", + "key": { + "dataType": "string", + "id": "TargetGroup--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "TargetGroup", + "type": "tag" + }, + "op": "nexists", + "value": "" + }, + { + "id": "234c77fd", + "key": { + "dataType": "string", + "id": "AvailabilityZone--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "AvailabilityZone", + "type": "tag" + }, + "op": "nexists", + "value": "" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + { + "dataType": "string", + "id": "LoadBalancer--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "LoadBalancer", + "type": "tag" + } + ], + "having": [], + "legend": "{{LoadBalancer}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "stepInterval": 60, + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "a1cc5b1c-adb6-4a71-bb6e-99337ef6a9d4", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "aws_ApplicationELB_HTTPCode_Target_5XX_Count_sum{TargetGroup=\"\"}" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Target 5XX", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The number of HTTP 5XX server error codes that originate from the load balancer. This count does not include any response codes generated by the targets.\n\nSee HTTPCode_ELB_5XX_Count at https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-cloudwatch-metrics.html", + "fillSpans": false, + "id": "e16fb999-491b-4cfa-b9aa-d558f2132b6b", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApplicationELB_HTTPCode_ELB_5XX_Count_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApplicationELB_HTTPCode_ELB_5XX_Count_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "5807a1e3", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "0dd63d0c", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "31ccfbae", + "key": { + "dataType": "string", + "id": "AvailabilityZone--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "AvailabilityZone", + "type": "tag" + }, + "op": "nexists", + "value": "" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "LoadBalancer--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "LoadBalancer", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + } + ], + "having": [], + "legend": "{{LoadBalancer}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "stepInterval": 60, + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "91d000c1-7697-4219-acb6-a43a5d455834", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Loadbalancer 5XX", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The total number of concurrent TCP connections active from clients to the load balancer and from the load balancer to targets.\n\nSee ActiveConnectionCount at https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-cloudwatch-metrics.html", + "fillSpans": false, + "id": "2be35406-693a-435b-a844-2df239be0b60", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApplicationELB_ActiveConnectionCount_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApplicationELB_ActiveConnectionCount_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "72c256c0", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "b433c2a1", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "8f5e7de0", + "key": { + "dataType": "string", + "id": "AvailabilityZone--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "AvailabilityZone", + "type": "tag" + }, + "op": "nexists", + "value": "" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "LoadBalancer--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "LoadBalancer", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + } + ], + "having": [], + "legend": "{{LoadBalancer}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "stepInterval": 60, + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "74429f26-2fe0-46f2-8b11-23c4f00a260a", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Active Connections", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The number of connections that were not successfully established between the load balancer and target. This metric does not apply if the target is a Lambda function. This metric is not incremented for unsuccessful health check connections.\n\nSee TargetConnectionErrorCount at https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-cloudwatch-metrics.html", + "fillSpans": false, + "id": "480ecee2-1271-4dfd-a7bb-9f9845957c6e", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApplicationELB_TargetConnectionErrorCount_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApplicationELB_TargetConnectionErrorCount_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "9226a37c", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "c3ff0c8f", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "e3317bc2", + "key": { + "dataType": "", + "isColumn": false, + "key": "TargetGroup", + "type": "" + }, + "op": "nexists", + "value": "" + }, + { + "id": "4e5c2324", + "key": { + "dataType": "", + "isColumn": false, + "key": "AvailabilityZone", + "type": "" + }, + "op": "nexists", + "value": "" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "LoadBalancer--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "LoadBalancer", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + } + ], + "having": [], + "legend": "{{LoadBalancer}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "stepInterval": 60, + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "148daebb-8ae4-4a9f-b569-5e0e75f6b52d", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Target Connection Errors", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The number of load balancer capacity units (LCU) used by your load balancer. You pay for the number of LCUs that you use per hour. When LCU reservation is active, ConsumedLCUs will report 0 if usage is below the reserved capacity, and will report values above 0 if usage exceeds the reserved LCUs\n\nSee ConsumedLCUs at https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-cloudwatch-metrics.html", + "fillSpans": false, + "id": "2243e542-0bbc-4e2a-a4dd-2e121abc9b95", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApplicationELB_ConsumedLCUs_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApplicationELB_ConsumedLCUs_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "20627274", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "cd861e27", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "LoadBalancer--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "LoadBalancer", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + } + ], + "having": [], + "legend": "{{LoadBalancer}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "stepInterval": 60, + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "38594221-7a5b-4d94-8775-26297f6cb882", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Consumed LCUs", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The total number of bytes processed by the load balancer over IPv4 and IPv6 (HTTP header and HTTP payload). This count includes traffic to and from clients and Lambda functions, and traffic from an Identity Provider (IdP) if user authentication is enabled.\n\nSee ProcessedBytes at https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-cloudwatch-metrics.html", + "fillSpans": false, + "id": "3bb56361-9a67-47ce-b186-ccee02e15f51", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApplicationELB_ProcessedBytes_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApplicationELB_ProcessedBytes_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "7d4a3494", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "3c307858", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "fbca8724", + "key": { + "dataType": "string", + "id": "AvailabilityZone--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "AvailabilityZone", + "type": "tag" + }, + "op": "nexists", + "value": "" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "LoadBalancer--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "LoadBalancer", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + } + ], + "having": [], + "legend": "{{LoadBalancer}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "stepInterval": 60, + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "e71d1223-7441-4a5d-b288-928a2903737d", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Processed Bytes", + "yAxisUnit": "bytes" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The maximum number of load balancer capacity units (LCU) used by your load balancer at a given point in time. Only applicable when using LCU Reservation.\n\nSee PeakLCUs at https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-cloudwatch-metrics.html", + "fillSpans": false, + "id": "36cbc321-6c02-4d13-895e-955d71d376b4", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApplicationELB_PeakLCUs_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApplicationELB_PeakLCUs_sum", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "a416e862", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "ed7d0a39", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "LoadBalancer--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "LoadBalancer", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + } + ], + "having": [], + "legend": "{{LoadBalancer}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "c4728417-0c3c-4477-b8bd-0a2de16c342a", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Peak LCUs", + "yAxisUnit": "none" + } + ] +} diff --git a/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/alb/assets/dashboards/overview.png b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/alb/assets/dashboards/overview.png new file mode 100644 index 0000000000000000000000000000000000000000..8005a6219ae86dc6bca5c3de185de0c946a3b806 GIT binary patch literal 128236 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�MQW#f&4ulHUX-lMq%MFOlXkHlRQ7 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
9%PYoE45uX8t4*j5aeS6!STAJ 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 literal 0 HcmV?d00001 diff --git a/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/alb/assets/dashboards/overview_dot.json b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/alb/assets/dashboards/overview_dot.json new file mode 100644 index 00000000000..e56d5e304aa --- /dev/null +++ b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/alb/assets/dashboards/overview_dot.json @@ -0,0 +1,1887 @@ +{ + "description": "Overview of Application Load Balancers", + "image": "data:image/svg+xml;base64,CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmlld0JveD0iMCAwIDg1IDg1IiBmaWxsPSIjZmZmIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48dXNlIHhsaW5rOmhyZWY9IiNBIiB4PSIyLjUiIHk9IjIuNSIvPjxzeW1ib2wgaWQ9IkEiIG92ZXJmbG93PSJ2aXNpYmxlIj48ZyBzdHJva2U9Im5vbmUiPjxwYXRoIGQ9Ik0wIDQxLjU3OUMwIDIwLjI5MyAxNy44NCAzLjE1NyA0MCAzLjE1N3M0MCAxNy4xMzYgNDAgMzguNDIyUzYyLjE2IDgwIDQwIDgwIDAgNjIuODY0IDAgNDEuNTc5eiIgZmlsbD0iIzlkNTAyNSIvPjxwYXRoIGQ9Ik0wIDM4LjQyMkMwIDE3LjEzNiAxNy44NCAwIDQwIDBzNDAgMTcuMTM2IDQwIDM4LjQyMi0xNy44NCAzOC40MjItNDAgMzguNDIyUzAgNTkuNzA3IDAgMzguNDIyeiIgZmlsbD0iI2Y1ODUzNiIvPjxwYXRoIGQ9Ik01MS42NzIgNy4zODd2MTMuOTUySDI4LjMyN1Y3LjM4N3ptMTguMDYxIDQwLjM3OHYxMS4zNjRoLTExLjgzVjQ3Ljc2NXptLTE0Ljk1OCAwdjExLjM2NGgtMTEuODNWNDcuNzY1em0tMTguMjA2IDB2MTEuMzY0aC0xMS44M1Y0Ny43NjV6bS0xNC45NTkgMHYxMS4zNjRIOS43OFY0Ny43NjV6Ii8+PHBhdGggZD0iTTE0LjYzIDM3LjkyOWgyLjEzdjExLjE0OWgtMi4xM3oiLz48cGF0aCBkPSJNMTQuNjMgMzcuOTI5aDE3LjA4OHYyLjA0NUgxNC42M3oiLz48cGF0aCBkPSJNMjkuNTg5IDM3LjkyOWgyLjEzdjExLjE0OUgyOS41OXptMTguMjA2IDBoMi4xM3YxMS4xNDloLTIuMTN6Ii8+PHBhdGggZD0iTTQ3Ljc5NSAzNy45MjloMTcuMDg4djIuMDQ1SDQ3Ljc5NXoiLz48cGF0aCBkPSJNNjIuNzU0IDM3LjkyOWgyLjEzdjExLjE0OWgtMi4xMjl6bS00MC42MzEtNy45NTRoMi4xM3Y4Ljk3N2gtMi4xM3pNMzguOTM1IDE5LjI4aDIuMTN2MTAuODU5aC0yLjEyOXoiLz48cGF0aCBkPSJNMjIuMTIzIDI5LjExNmgzNS4zMnYyLjA0NWgtMzUuMzJ6Ii8+PHBhdGggZD0iTTU1LjMxNCAyOS45NzVoMi4xM3Y4Ljk3N2gtMi4xMjl6Ii8+PC9nPjwvc3ltYm9sPjwvc3ZnPg==", + "layout": [ + { + "h": 5, + "i": "e13232f0-6308-4466-94c0-629cae762ff0", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 0 + }, + { + "h": 5, + "i": "9b99d70a-f12a-4df7-9a68-660a0ab55e42", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 0 + }, + { + "h": 5, + "i": "5a9ec75f-3bcd-4829-94e6-452caa2cc0d2", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 5 + }, + { + "h": 5, + "i": "e16fb999-491b-4cfa-b9aa-d558f2132b6b", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 5 + }, + { + "h": 5, + "i": "2be35406-693a-435b-a844-2df239be0b60", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 10 + }, + { + "h": 5, + "i": "480ecee2-1271-4dfd-a7bb-9f9845957c6e", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 10 + }, + { + "h": 5, + "i": "2243e542-0bbc-4e2a-a4dd-2e121abc9b95", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 15 + }, + { + "h": 5, + "i": "3bb56361-9a67-47ce-b186-ccee02e15f51", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 15 + }, + { + "h": 5, + "i": "36cbc321-6c02-4d13-895e-955d71d376b4", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 20 + } + ], + "panelMap": {}, + "tags": [], + "title": "Application Load Balancer Overview", + "uploadedGrafana": false, + "variables": { + "Account": { + "allSelected": false, + "customValue": "", + "description": "AWS Account", + "id": "d5ef5880-68b1-4097-b4e5-9ce74200831f", + "key": "d5ef5880-68b1-4097-b4e5-9ce74200831f", + "modificationUUID": "9974ddda-3bc1-401d-b04a-364ea9a23866", + "multiSelect": false, + "name": "Account", + "order": 0, + "queryValue": "SELECT JSONExtractString(labels, 'cloud.account.id') as `cloud.account.id`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like 'aws_ApplicationELB_ConsumedLCUs_max'\nGROUP BY `cloud.account.id`", + "showALLOption": false, + "sort": "DISABLED", + "textboxValue": "", + "type": "QUERY" + }, + "Region": { + "allSelected": false, + "customValue": "", + "description": "AWS Region", + "id": "bad33abd-ab38-493b-b23e-131659b6d03c", + "key": "bad33abd-ab38-493b-b23e-131659b6d03c", + "modificationUUID": "1aa1ef37-260c-4ca9-8aa9-f2ffb289c9e3", + "multiSelect": false, + "name": "Region", + "order": 1, + "queryValue": "SELECT JSONExtractString(labels, 'cloud.region') as `cloud.region`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like 'aws_ApplicationELB_ConsumedLCUs_max'\n and JSONExtractString(labels, 'cloud.account.id') IN {{.Account}}\nGROUP BY `cloud.region`\n", + "showALLOption": false, + "sort": "DISABLED", + "textboxValue": "", + "type": "QUERY" + } + }, + "version": "v4", + "widgets": [ + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The time elapsed, after the request leaves the load balancer until the target starts to send the response headers.\n\nSee TargetResponseTime at https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-cloudwatch-metrics.html", + "fillSpans": false, + "id": "9b99d70a-f12a-4df7-9a68-660a0ab55e42", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApplicationELB_TargetResponseTime_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApplicationELB_TargetResponseTime_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "b282d9f1", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "71837c70", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "5bfcc581", + "key": { + "dataType": "string", + "id": "TargetGroup--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "TargetGroup", + "type": "tag" + }, + "op": "nexists", + "value": "" + }, + { + "id": "a9e33e08", + "key": { + "dataType": "string", + "id": "AvailabilityZone--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "AvailabilityZone", + "type": "tag" + }, + "op": "nexists", + "value": "" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "LoadBalancer--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "LoadBalancer", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + } + ], + "having": [], + "legend": "{{LoadBalancer}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "601aca8a-36fb-4e6a-b234-39294b9b305b", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Target Response Time", + "yAxisUnit": "s" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The number of requests processed over IPv4 and IPv6. This metric is only incremented for requests where the load balancer node was able to choose a target. Requests that are rejected before a target is chosen are not reflected in this metric.\n\nSee RequestCount at https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-cloudwatch-metrics.html", + "fillSpans": false, + "id": "e13232f0-6308-4466-94c0-629cae762ff0", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApplicationELB_RequestCount_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApplicationELB_RequestCount_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "448b551a", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "a8821216", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "f5a62c5a", + "key": { + "dataType": "string", + "id": "AvailabilityZone--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "AvailabilityZone", + "type": "tag" + }, + "op": "nexists", + "value": "" + }, + { + "id": "25e8abc8", + "key": { + "dataType": "string", + "id": "TargetGroup--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "TargetGroup", + "type": "tag" + }, + "op": "nexists", + "value": "" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "LoadBalancer--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "LoadBalancer", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + } + ], + "having": [], + "legend": "{{LoadBalancer}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "stepInterval": 60, + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "7d92a02f-3202-4059-8b88-2d24112a35e6", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Requests", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The number of HTTP 5XX response codes generated by the targets. This does not include any response codes generated by the load balancer.\n\nSee HTTPCode_Target_5XX_Count at https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-cloudwatch-metrics.html", + "fillSpans": false, + "id": "5a9ec75f-3bcd-4829-94e6-452caa2cc0d2", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApplicationELB_HTTPCode_Target_5XX_Count_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApplicationELB_HTTPCode_Target_5XX_Count_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "702a8765", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "32985f2d", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "e4cf3d8b", + "key": { + "dataType": "string", + "id": "TargetGroup--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "TargetGroup", + "type": "tag" + }, + "op": "nexists", + "value": "" + }, + { + "id": "234c77fd", + "key": { + "dataType": "string", + "id": "AvailabilityZone--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "AvailabilityZone", + "type": "tag" + }, + "op": "nexists", + "value": "" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + { + "dataType": "string", + "id": "LoadBalancer--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "LoadBalancer", + "type": "tag" + } + ], + "having": [], + "legend": "{{LoadBalancer}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "stepInterval": 60, + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "a1cc5b1c-adb6-4a71-bb6e-99337ef6a9d4", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "aws_ApplicationELB_HTTPCode_Target_5XX_Count_sum{TargetGroup=\"\"}" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Target 5XX", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The number of HTTP 5XX server error codes that originate from the load balancer. This count does not include any response codes generated by the targets.\n\nSee HTTPCode_ELB_5XX_Count at https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-cloudwatch-metrics.html", + "fillSpans": false, + "id": "e16fb999-491b-4cfa-b9aa-d558f2132b6b", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApplicationELB_HTTPCode_ELB_5XX_Count_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApplicationELB_HTTPCode_ELB_5XX_Count_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "5807a1e3", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "0dd63d0c", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "31ccfbae", + "key": { + "dataType": "string", + "id": "AvailabilityZone--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "AvailabilityZone", + "type": "tag" + }, + "op": "nexists", + "value": "" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "LoadBalancer--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "LoadBalancer", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + } + ], + "having": [], + "legend": "{{LoadBalancer}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "stepInterval": 60, + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "91d000c1-7697-4219-acb6-a43a5d455834", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Loadbalancer 5XX", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The total number of concurrent TCP connections active from clients to the load balancer and from the load balancer to targets.\n\nSee ActiveConnectionCount at https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-cloudwatch-metrics.html", + "fillSpans": false, + "id": "2be35406-693a-435b-a844-2df239be0b60", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApplicationELB_ActiveConnectionCount_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApplicationELB_ActiveConnectionCount_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "72c256c0", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "b433c2a1", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "8f5e7de0", + "key": { + "dataType": "string", + "id": "AvailabilityZone--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "AvailabilityZone", + "type": "tag" + }, + "op": "nexists", + "value": "" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "LoadBalancer--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "LoadBalancer", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + } + ], + "having": [], + "legend": "{{LoadBalancer}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "stepInterval": 60, + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "74429f26-2fe0-46f2-8b11-23c4f00a260a", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Active Connections", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The number of connections that were not successfully established between the load balancer and target. This metric does not apply if the target is a Lambda function. This metric is not incremented for unsuccessful health check connections.\n\nSee TargetConnectionErrorCount at https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-cloudwatch-metrics.html", + "fillSpans": false, + "id": "480ecee2-1271-4dfd-a7bb-9f9845957c6e", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApplicationELB_TargetConnectionErrorCount_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApplicationELB_TargetConnectionErrorCount_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "9226a37c", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "c3ff0c8f", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "e3317bc2", + "key": { + "dataType": "", + "isColumn": false, + "key": "TargetGroup", + "type": "" + }, + "op": "nexists", + "value": "" + }, + { + "id": "4e5c2324", + "key": { + "dataType": "", + "isColumn": false, + "key": "AvailabilityZone", + "type": "" + }, + "op": "nexists", + "value": "" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "LoadBalancer--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "LoadBalancer", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + } + ], + "having": [], + "legend": "{{LoadBalancer}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "stepInterval": 60, + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "148daebb-8ae4-4a9f-b569-5e0e75f6b52d", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Target Connection Errors", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The number of load balancer capacity units (LCU) used by your load balancer. You pay for the number of LCUs that you use per hour. When LCU reservation is active, ConsumedLCUs will report 0 if usage is below the reserved capacity, and will report values above 0 if usage exceeds the reserved LCUs\n\nSee ConsumedLCUs at https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-cloudwatch-metrics.html", + "fillSpans": false, + "id": "2243e542-0bbc-4e2a-a4dd-2e121abc9b95", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApplicationELB_ConsumedLCUs_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApplicationELB_ConsumedLCUs_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "20627274", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "cd861e27", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "LoadBalancer--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "LoadBalancer", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + } + ], + "having": [], + "legend": "{{LoadBalancer}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "stepInterval": 60, + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "38594221-7a5b-4d94-8775-26297f6cb882", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Consumed LCUs", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The total number of bytes processed by the load balancer over IPv4 and IPv6 (HTTP header and HTTP payload). This count includes traffic to and from clients and Lambda functions, and traffic from an Identity Provider (IdP) if user authentication is enabled.\n\nSee ProcessedBytes at https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-cloudwatch-metrics.html", + "fillSpans": false, + "id": "3bb56361-9a67-47ce-b186-ccee02e15f51", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApplicationELB_ProcessedBytes_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApplicationELB_ProcessedBytes_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "7d4a3494", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "3c307858", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "fbca8724", + "key": { + "dataType": "string", + "id": "AvailabilityZone--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "AvailabilityZone", + "type": "tag" + }, + "op": "nexists", + "value": "" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "LoadBalancer--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "LoadBalancer", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + } + ], + "having": [], + "legend": "{{LoadBalancer}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "stepInterval": 60, + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "e71d1223-7441-4a5d-b288-928a2903737d", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Processed Bytes", + "yAxisUnit": "bytes" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The maximum number of load balancer capacity units (LCU) used by your load balancer at a given point in time. Only applicable when using LCU Reservation.\n\nSee PeakLCUs at https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-cloudwatch-metrics.html", + "fillSpans": false, + "id": "36cbc321-6c02-4d13-895e-955d71d376b4", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApplicationELB_PeakLCUs_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApplicationELB_PeakLCUs_sum", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "a416e862", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "ed7d0a39", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "LoadBalancer--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "LoadBalancer", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + } + ], + "having": [], + "legend": "{{LoadBalancer}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "c4728417-0c3c-4477-b8bd-0a2de16c342a", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Peak LCUs", + "yAxisUnit": "none" + } + ] +} diff --git a/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/alb/icon.svg b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/alb/icon.svg new file mode 100644 index 00000000000..d2c8b0ba6f5 --- /dev/null +++ b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/alb/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/alb/integration.json b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/alb/integration.json new file mode 100644 index 00000000000..6c3017810dc --- /dev/null +++ b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/alb/integration.json @@ -0,0 +1,468 @@ +{ + "id": "alb", + "title": "ALB", + "icon": "file://icon.svg", + "overview": "file://overview.md", + "supportedSignals": { + "metrics": true, + "logs": false + }, + "dataCollected": { + "metrics": [ + { + "name": "aws_ApplicationELB_ActiveConnectionCount_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_ActiveConnectionCount_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_ActiveConnectionCount_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_ActiveConnectionCount_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_AnomalousHostCount_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_AnomalousHostCount_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_AnomalousHostCount_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_AnomalousHostCount_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_ConsumedLCUs_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_ConsumedLCUs_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_ConsumedLCUs_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_ConsumedLCUs_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_HTTPCode_Target_2XX_Count_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_HTTPCode_Target_2XX_Count_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_HTTPCode_Target_2XX_Count_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_HTTPCode_Target_2XX_Count_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_HTTPCode_Target_4XX_Count_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_HTTPCode_Target_4XX_Count_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_HTTPCode_Target_4XX_Count_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_HTTPCode_Target_4XX_Count_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_HealthyHostCount_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_HealthyHostCount_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_HealthyHostCount_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_HealthyHostCount_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_HealthyStateDNS_count", + "unit": "None", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_HealthyStateDNS_max", + "unit": "None", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_HealthyStateDNS_min", + "unit": "None", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_HealthyStateDNS_sum", + "unit": "None", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_HealthyStateRouting_count", + "unit": "None", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_HealthyStateRouting_max", + "unit": "None", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_HealthyStateRouting_min", + "unit": "None", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_HealthyStateRouting_sum", + "unit": "None", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_MitigatedHostCount_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_MitigatedHostCount_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_MitigatedHostCount_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_MitigatedHostCount_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_NewConnectionCount_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_NewConnectionCount_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_NewConnectionCount_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_NewConnectionCount_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_PeakLCUs_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_PeakLCUs_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_PeakLCUs_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_PeakLCUs_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_ProcessedBytes_count", + "unit": "Bytes", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_ProcessedBytes_max", + "unit": "Bytes", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_ProcessedBytes_min", + "unit": "Bytes", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_ProcessedBytes_sum", + "unit": "Bytes", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_RequestCountPerTarget_count", + "unit": "None", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_RequestCountPerTarget_max", + "unit": "None", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_RequestCountPerTarget_min", + "unit": "None", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_RequestCountPerTarget_sum", + "unit": "None", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_RequestCount_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_RequestCount_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_RequestCount_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_RequestCount_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_TargetResponseTime_count", + "unit": "Seconds", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_TargetResponseTime_max", + "unit": "Seconds", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_TargetResponseTime_min", + "unit": "Seconds", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_TargetResponseTime_sum", + "unit": "Seconds", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_UnHealthyHostCount_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_UnHealthyHostCount_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_UnHealthyHostCount_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_UnHealthyHostCount_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_UnhealthyStateDNS_count", + "unit": "None", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_UnhealthyStateDNS_max", + "unit": "None", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_UnhealthyStateDNS_min", + "unit": "None", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_UnhealthyStateDNS_sum", + "unit": "None", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_UnhealthyStateRouting_count", + "unit": "None", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_UnhealthyStateRouting_max", + "unit": "None", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_UnhealthyStateRouting_min", + "unit": "None", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_ApplicationELB_UnhealthyStateRouting_sum", + "unit": "None", + "type": "Gauge", + "description": "" + } + ], + "logs": [] + }, + "telemetryCollectionStrategy": { + "aws": { + "metrics": { + "cloudwatchMetricStreamFilters": [ + { + "Namespace": "AWS/ApplicationELB" + } + ] + } + } + }, + "assets": { + "dashboards": [ + { + "id": "overview", + "title": "ALB Overview", + "description": "Overview of Application Load Balancer", + "definition": "file://assets/dashboards/overview.json" + } + ] + } +} diff --git a/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/alb/overview.md b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/alb/overview.md new file mode 100644 index 00000000000..29633b229cd --- /dev/null +++ b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/alb/overview.md @@ -0,0 +1,3 @@ +### Monitor Application Load Balancers with SigNoz + +Collect key ALB metrics and view them with an out of the box dashboard. diff --git a/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/api-gateway/assets/dashboards/overview.json b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/api-gateway/assets/dashboards/overview.json new file mode 100644 index 00000000000..ab46815e9a1 --- /dev/null +++ b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/api-gateway/assets/dashboards/overview.json @@ -0,0 +1,2269 @@ +{ + "description": "Overview of API Gateway resources in a region, supporting REST, HTTP, and WebSocket APIs.", + "image": "data:image/svg+xml,%3Csvg%20width%3D%2224px%22%20height%3D%2224px%22%20viewBox%3D%220%200%2024%2024%22%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%3E%3Cdefs%3E%3ClinearGradient%20x1%3D%220%25%22%20y1%3D%22100%25%22%20x2%3D%22100%25%22%20y2%3D%220%25%22%20id%3D%22linearGradient-1%22%3E%3Cstop%20stop-color%3D%22%234D27A8%22%20offset%3D%220%25%22%3E%3C%2Fstop%3E%3Cstop%20stop-color%3D%22%23A166FF%22%20offset%3D%22100%25%22%3E%3C%2Fstop%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Cg%20stroke%3D%22none%22%20stroke-width%3D%221%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Cg%20fill%3D%22url(%23linearGradient-1)%22%3E%3Crect%20id%3D%22Rectangle%22%20x%3D%220%22%20y%3D%220%22%20width%3D%2224%22%20height%3D%2224%22%3E%3C%2Frect%3E%3C%2Fg%3E%3Cpath%20d%3D%22M6%2C6.76751613%20L8%2C5.43446738%20L8%2C18.5659476%20L6%2C17.2328988%20L6%2C6.76751613%20Z%20M5%2C6.49950633%20L5%2C17.4999086%20C5%2C17.6669147%205.084%2C17.8239204%205.223%2C17.9159238%20L8.223%2C19.9159969%20C8.307%2C19.971999%208.403%2C20%208.5%2C20%20C8.581%2C20%208.662%2C19.9809993%208.736%2C19.9409978%20C8.898%2C19.8539947%209%2C19.6849885%209%2C19.4999817%20L9%2C16.9998903%20L10%2C16.9998903%20L10%2C15.9998537%20L9%2C15.9998537%20L9%2C7.99956118%20L10%2C7.99956118%20L10%2C6.99952461%20L9%2C6.99952461%20L9%2C4.49943319%20C9%2C4.31542646%208.898%2C4.14542025%208.736%2C4.0594171%20C8.574%2C3.97241392%208.377%2C3.98141425%208.223%2C4.08341798%20L5.223%2C6.08349112%20C5.084%2C6.17649452%205%2C6.33250022%205%2C6.49950633%20L5%2C6.49950633%20Z%20M19%2C17.2328988%20L17%2C18.5659476%20L17%2C5.43446738%20L19%2C6.76751613%20L19%2C17.2328988%20Z%20M19.777%2C6.08349112%20L16.777%2C4.08341798%20C16.623%2C3.98141425%2016.426%2C3.97241392%2016.264%2C4.0594171%20C16.102%2C4.14542025%2016%2C4.31542646%2016%2C4.49943319%20L16%2C6.99952461%20L15%2C6.99952461%20L15%2C7.99956118%20L16%2C7.99956118%20L16%2C15.9998537%20L15%2C15.9998537%20L15%2C16.9998903%20L16%2C16.9998903%20L16%2C19.4999817%20C16%2C19.6849885%2016.102%2C19.8539947%2016.264%2C19.9409978%20C16.338%2C19.9809993%2016.419%2C20%2016.5%2C20%20C16.597%2C20%2016.693%2C19.971999%2016.777%2C19.9159969%20L19.777%2C17.9159238%20C19.916%2C17.8239204%2020%2C17.6669147%2020%2C17.4999086%20L20%2C6.49950633%20C20%2C6.33250022%2019.916%2C6.17649452%2019.777%2C6.08349112%20L19.777%2C6.08349112%20Z%20M13%2C7.99956118%20L14%2C7.99956118%20L14%2C6.99952461%20L13%2C6.99952461%20L13%2C7.99956118%20Z%20M11%2C7.99956118%20L12%2C7.99956118%20L12%2C6.99952461%20L11%2C6.99952461%20L11%2C7.99956118%20Z%20M13%2C16.9998903%20L14%2C16.9998903%20L14%2C15.9998537%20L13%2C15.9998537%20L13%2C16.9998903%20Z%20M11%2C16.9998903%20L12%2C16.9998903%20L12%2C15.9998537%20L11%2C15.9998537%20L11%2C16.9998903%20Z%20M13.18%2C14.884813%20L10.18%2C12.3847215%20C10.065%2C12.288718%2010%2C12.1487129%2010%2C11.9997075%20C10%2C11.851702%2010.065%2C11.7106969%2010.18%2C11.6156934%20L13.18%2C9.11560199%20L13.82%2C9.88463011%20L11.281%2C11.9997075%20L13.82%2C14.1157848%20L13.18%2C14.884813%20Z%22%20id%3D%22Amazon-API-Gateway_Icon_16_Squid%22%20fill%3D%22%23FFFFFF%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E", + "layout": [ + { + "h": 1, + "i": "row_rest", + "maxH": 1, + "minH": 1, + "minW": 12, + "moved": false, + "static": false, + "w": 12, + "x": 0, + "y": 0 + }, + { + "h": 6, + "i": "rest_requests", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 1 + }, + { + "h": 6, + "i": "rest_latency", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 1 + }, + { + "h": 6, + "i": "rest_5xx", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 7 + }, + { + "h": 6, + "i": "rest_integration_latency", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 7 + }, + { + "h": 6, + "i": "rest_cache_hit", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 13 + }, + { + "h": 6, + "i": "rest_cache_miss", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 13 + }, + { + "h": 1, + "i": "row_http", + "maxH": 1, + "minH": 1, + "minW": 12, + "moved": false, + "static": false, + "w": 12, + "x": 0, + "y": 19 + }, + { + "h": 6, + "i": "http_count", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 20 + }, + { + "h": 6, + "i": "http_4xx", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 20 + }, + { + "h": 6, + "i": "http_5xx", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 26 + }, + { + "h": 6, + "i": "http_latency", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 26 + }, + { + "h": 6, + "i": "http_data_processed", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 32 + }, + { + "h": 1, + "i": "row_websocket", + "maxH": 1, + "minH": 1, + "minW": 12, + "moved": false, + "static": false, + "w": 12, + "x": 0, + "y": 38 + }, + { + "h": 6, + "i": "ws_connect_count", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 39 + }, + { + "h": 6, + "i": "ws_message_count", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 39 + }, + { + "h": 6, + "i": "ws_client_error", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 45 + }, + { + "h": 6, + "i": "ws_execution_error", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 45 + }, + { + "h": 6, + "i": "ws_integration_error", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 51 + }, + { + "h": 1, + "i": "row_common", + "maxH": 1, + "minH": 1, + "minW": 12, + "moved": false, + "static": false, + "w": 12, + "x": 0, + "y": 57 + }, + { + "h": 6, + "i": "common_integration_latency", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 58 + } + ], + "panelMap": { + "row_rest": { + "collapsed": false, + "widgets": [ + { + "h": 6, + "i": "rest_requests", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 1 + }, + { + "h": 6, + "i": "rest_latency", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 1 + }, + { + "h": 6, + "i": "rest_5xx", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 7 + }, + { + "h": 6, + "i": "rest_integration_latency", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 7 + }, + { + "h": 6, + "i": "rest_cache_hit", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 13 + }, + { + "h": 6, + "i": "rest_cache_miss", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 13 + } + ] + }, + "row_http": { + "collapsed": false, + "widgets": [ + { + "h": 6, + "i": "http_count", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 20 + }, + { + "h": 6, + "i": "http_4xx", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 20 + }, + { + "h": 6, + "i": "http_5xx", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 26 + }, + { + "h": 6, + "i": "http_latency", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 26 + }, + { + "h": 6, + "i": "http_data_processed", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 32 + } + ] + }, + "row_websocket": { + "collapsed": false, + "widgets": [ + { + "h": 6, + "i": "ws_connect_count", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 39 + }, + { + "h": 6, + "i": "ws_message_count", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 39 + }, + { + "h": 6, + "i": "ws_client_error", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 45 + }, + { + "h": 6, + "i": "ws_execution_error", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 45 + }, + { + "h": 6, + "i": "ws_integration_error", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 51 + } + ] + }, + "row_common": { + "collapsed": false, + "widgets": [ + { + "h": 6, + "i": "common_integration_latency", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 58 + } + ] + } + }, + "tags": [], + "title": "API Gateway Overview (Multi-API)", + "uploadedGrafana": false, + "variables": { + "Account": { + "allSelected": false, + "customValue": "", + "description": "AWS Account", + "id": "93447e60-3b35-407c-a86c-97254f641628", + "key": "93447e60-3b35-407c-a86c-97254f641628", + "modificationUUID": "5e1f8a79-e9b5-4230-af48-f2c117805b31", + "multiSelect": false, + "name": "Account", + "order": 0, + "queryValue": "SELECT JSONExtractString(labels, 'cloud_account_id') as `cloud_account_id`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like '%aws_ApiGateway%'\nGROUP BY `cloud_account_id`", + "showALLOption": false, + "sort": "DISABLED", + "textboxValue": "", + "type": "QUERY" + }, + "Region": { + "allSelected": false, + "customValue": "", + "description": "AWS Region", + "id": "613f7913-ace1-4cf5-bb4c-df91878ec40e", + "key": "613f7913-ace1-4cf5-bb4c-df91878ec40e", + "modificationUUID": "bb86427a-e4da-4440-93d3-0f53e65214ed", + "multiSelect": false, + "name": "Region", + "order": 1, + "queryValue": "SELECT JSONExtractString(labels, 'cloud_region') as `cloud_region`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like '%aws_ApiGateway%'\n and JSONExtractString(labels, 'cloud_account_id') IN {{.Account}}\nGROUP BY `cloud_region`", + "showALLOption": false, + "sort": "DISABLED", + "textboxValue": "", + "type": "QUERY" + } + }, + "version": "v4", + "widgets": [ + { + "description": "", + "id": "row_rest", + "panelTypes": "row", + "title": "REST API Metrics" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The total number API requests in a given period.", + "fillSpans": false, + "id": "rest_requests", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_Count_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_Count_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "d3e9ea95", + "key": { + "dataType": "string", + "id": "ApiName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiName", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "81918ce2", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "114c7ff4", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiName", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiName}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "id": "rest-requests-query", + "queryType": "builder" + }, + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Requests", + "yAxisUnit": "cpm" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The time between when API Gateway receives a request from a client and when it returns a response to the client.", + "fillSpans": false, + "id": "rest_latency", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_Latency_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_Latency_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "4b55a919", + "key": { + "dataType": "string", + "id": "ApiName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiName", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "3c67d1fc", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "e2a96f23", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiName", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiName}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "id": "rest-latency-query", + "queryType": "builder" + }, + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Latency", + "yAxisUnit": "ms" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The number of server-side errors captured in a given period.", + "fillSpans": false, + "id": "rest_5xx", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_5XXError_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_5XXError_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "3e4a6042", + "key": { + "dataType": "string", + "id": "ApiName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiName", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "b3ebaf28", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "f2030d94", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiName", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiName}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "id": "rest-5xx-query", + "queryType": "builder" + }, + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "5XX Errors", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The time between when API Gateway relays a request to the backend and when it receives a response from the backend.", + "fillSpans": false, + "id": "rest_integration_latency", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_IntegrationLatency_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_IntegrationLatency_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "1a3f91b4", + "key": { + "dataType": "string", + "id": "ApiName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiName", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "5f2f2892", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "960ee6d2", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiName", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiName}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "id": "rest-integ-latency-query", + "queryType": "builder" + }, + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Integration Latency", + "yAxisUnit": "ms" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The number of requests served from the API cache in a given period.\n\nSee CacheHitCount at https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-metrics-and-dimensions.html for more details", + "fillSpans": false, + "id": "rest_cache_hit", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_CacheHitCount_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_CacheHitCount_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "8016ef52", + "key": { + "dataType": "string", + "id": "ApiName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiName", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "8ec09e30", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "d1622884", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiName", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiName}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "id": "rest-cache-hit-query", + "queryType": "builder" + }, + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Cache Hits", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The number of requests served from the backend in a given period, when API caching is enabled.\n\nSee CacheMissCount at https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-metrics-and-dimensions.html for more details", + "fillSpans": false, + "id": "rest_cache_miss", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_CacheMissCount_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_CacheMissCount_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "6cbbb46e", + "key": { + "dataType": "string", + "id": "ApiName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiName", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "f02d484d", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "4d8ddc75", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiName", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiName}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "id": "rest-cache-miss-query", + "queryType": "builder" + }, + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Cache Misses", + "yAxisUnit": "none" + }, + { + "description": "", + "id": "row_http", + "panelTypes": "row", + "title": "HTTP API Metrics" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The total number of HTTP API requests in a given period.", + "fillSpans": false, + "id": "http_count", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_Count_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_Count_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "http1", + "key": { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "http2", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "http3", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiId}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "id": "http-count-query", + "queryType": "builder" + }, + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Requests", + "yAxisUnit": "cpm" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The number of client-side errors (4XX errors) in a given period.", + "fillSpans": false, + "id": "http_4xx", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_4xx_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_4xx_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "http4xx1", + "key": { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "http4xx2", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "http4xx3", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiId}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "id": "http-4xx-query", + "queryType": "builder" + }, + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "4XX Errors", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The number of server-side errors (5XX errors) in a given period.", + "fillSpans": false, + "id": "http_5xx", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_5xx_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_5xx_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "http5xx1", + "key": { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "http5xx2", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "http5xx3", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiId}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "id": "http-5xx-query", + "queryType": "builder" + }, + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "5XX Errors", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The total time between when API Gateway receives a request from a client and when it returns a response to the client.", + "fillSpans": false, + "id": "http_latency", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_Latency_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_Latency_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "httplat1", + "key": { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "httplat2", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "httplat3", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiId}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "id": "http-latency-query", + "queryType": "builder" + }, + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Latency", + "yAxisUnit": "ms" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The amount of data processed in bytes for HTTP API requests.", + "fillSpans": false, + "id": "http_data_processed", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_DataProcessed_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_DataProcessed_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "httpdata1", + "key": { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "httpdata2", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "httpdata3", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiId}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "id": "http-data-query", + "queryType": "builder" + }, + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Data Processed", + "yAxisUnit": "bytes" + }, + { + "description": "", + "id": "row_websocket", + "panelTypes": "row", + "title": "WebSocket API Metrics" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The number of messages sent to the $connect route integration.", + "fillSpans": false, + "id": "ws_connect_count", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_ConnectCount_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_ConnectCount_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "wscon1", + "key": { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "wscon2", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "wscon3", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiId}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "id": "ws-connect-query", + "queryType": "builder" + }, + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Connect Count", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The number of messages sent to your WebSocket API.", + "fillSpans": false, + "id": "ws_message_count", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_MessageCount_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_MessageCount_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "wsmsg1", + "key": { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "wsmsg2", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "wsmsg3", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiId}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "id": "ws-message-query", + "queryType": "builder" + }, + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Message Count", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The number of requests that return a 4XX response from API Gateway before the integration is invoked.", + "fillSpans": false, + "id": "ws_client_error", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_ClientError_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_ClientError_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "wscli1", + "key": { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "wscli2", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "wscli3", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiId}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "id": "ws-client-error-query", + "queryType": "builder" + }, + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Client Errors (4XX)", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "Errors that occurred when calling the integration (5XX responses).", + "fillSpans": false, + "id": "ws_execution_error", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_ExecutionError_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_ExecutionError_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "wsexec1", + "key": { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "wsexec2", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "wsexec3", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiId}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "id": "ws-exec-error-query", + "queryType": "builder" + }, + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Execution Errors (5XX)", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The number of requests that return a 4XX/5XX response from the integration.", + "fillSpans": false, + "id": "ws_integration_error", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_IntegrationError_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_IntegrationError_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "wsint1", + "key": { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "wsint2", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "wsint3", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiId}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "id": "ws-integ-error-query", + "queryType": "builder" + }, + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Integration Errors", + "yAxisUnit": "none" + }, + { + "description": "", + "id": "row_common", + "panelTypes": "row", + "title": "Common Metrics (HTTP & WebSocket APIs)" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The time between when API Gateway relays a request to the backend and when it receives a response from the backend. This metric applies to both HTTP and WebSocket APIs.", + "fillSpans": false, + "id": "common_integration_latency", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_IntegrationLatency_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_IntegrationLatency_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "commonintlat1", + "key": { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "commonintlat2", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "commonintlat3", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "commonintlat4", + "key": { + "dataType": "string", + "id": "Stage--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "Stage", + "type": "tag" + }, + "op": "exists", + "value": "" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + }, + { + "dataType": "string", + "id": "Stage--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "Stage", + "type": "tag" + } + ], + "having": [], + "legend": "{{Stage}} - {{ApiId}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "id": "common-integ-latency-query", + "queryType": "builder" + }, + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Integration Latency", + "yAxisUnit": "ms" + } + ] +} diff --git a/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/api-gateway/assets/dashboards/overview.png b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/api-gateway/assets/dashboards/overview.png new file mode 100644 index 0000000000000000000000000000000000000000..d57e5ae411715d59a4c978b597b9e764fab2a74b GIT binary patch literal 202518 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≀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

vJr=-6A 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÷q&{d%xs$zYk|QYlCJv%G$Nu`f75 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

pk`X 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 z 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 literal 0 HcmV?d00001 diff --git a/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/api-gateway/assets/dashboards/overview_dot.json b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/api-gateway/assets/dashboards/overview_dot.json new file mode 100644 index 00000000000..4cbb7c453eb --- /dev/null +++ b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/api-gateway/assets/dashboards/overview_dot.json @@ -0,0 +1,2286 @@ +{ + "description": "Overview of API Gateway resources in a region, supporting REST, HTTP, and WebSocket APIs.", + "image": "data:image/svg+xml,%3Csvg%20width%3D%2224px%22%20height%3D%2224px%22%20viewBox%3D%220%200%2024%2024%22%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%3E%3Cdefs%3E%3ClinearGradient%20x1%3D%220%25%22%20y1%3D%22100%25%22%20x2%3D%22100%25%22%20y2%3D%220%25%22%20id%3D%22linearGradient-1%22%3E%3Cstop%20stop-color%3D%22%234D27A8%22%20offset%3D%220%25%22%3E%3C%2Fstop%3E%3Cstop%20stop-color%3D%22%23A166FF%22%20offset%3D%22100%25%22%3E%3C%2Fstop%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Cg%20stroke%3D%22none%22%20stroke-width%3D%221%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Cg%20fill%3D%22url(%23linearGradient-1)%22%3E%3Crect%20id%3D%22Rectangle%22%20x%3D%220%22%20y%3D%220%22%20width%3D%2224%22%20height%3D%2224%22%3E%3C%2Frect%3E%3C%2Fg%3E%3Cpath%20d%3D%22M6%2C6.76751613%20L8%2C5.43446738%20L8%2C18.5659476%20L6%2C17.2328988%20L6%2C6.76751613%20Z%20M5%2C6.49950633%20L5%2C17.4999086%20C5%2C17.6669147%205.084%2C17.8239204%205.223%2C17.9159238%20L8.223%2C19.9159969%20C8.307%2C19.971999%208.403%2C20%208.5%2C20%20C8.581%2C20%208.662%2C19.9809993%208.736%2C19.9409978%20C8.898%2C19.8539947%209%2C19.6849885%209%2C19.4999817%20L9%2C16.9998903%20L10%2C16.9998903%20L10%2C15.9998537%20L9%2C15.9998537%20L9%2C7.99956118%20L10%2C7.99956118%20L10%2C6.99952461%20L9%2C6.99952461%20L9%2C4.49943319%20C9%2C4.31542646%208.898%2C4.14542025%208.736%2C4.0594171%20C8.574%2C3.97241392%208.377%2C3.98141425%208.223%2C4.08341798%20L5.223%2C6.08349112%20C5.084%2C6.17649452%205%2C6.33250022%205%2C6.49950633%20L5%2C6.49950633%20Z%20M19%2C17.2328988%20L17%2C18.5659476%20L17%2C5.43446738%20L19%2C6.76751613%20L19%2C17.2328988%20Z%20M19.777%2C6.08349112%20L16.777%2C4.08341798%20C16.623%2C3.98141425%2016.426%2C3.97241392%2016.264%2C4.0594171%20C16.102%2C4.14542025%2016%2C4.31542646%2016%2C4.49943319%20L16%2C6.99952461%20L15%2C6.99952461%20L15%2C7.99956118%20L16%2C7.99956118%20L16%2C15.9998537%20L15%2C15.9998537%20L15%2C16.9998903%20L16%2C16.9998903%20L16%2C19.4999817%20C16%2C19.6849885%2016.102%2C19.8539947%2016.264%2C19.9409978%20C16.338%2C19.9809993%2016.419%2C20%2016.5%2C20%20C16.597%2C20%2016.693%2C19.971999%2016.777%2C19.9159969%20L19.777%2C17.9159238%20C19.916%2C17.8239204%2020%2C17.6669147%2020%2C17.4999086%20L20%2C6.49950633%20C20%2C6.33250022%2019.916%2C6.17649452%2019.777%2C6.08349112%20L19.777%2C6.08349112%20Z%20M13%2C7.99956118%20L14%2C7.99956118%20L14%2C6.99952461%20L13%2C6.99952461%20L13%2C7.99956118%20Z%20M11%2C7.99956118%20L12%2C7.99956118%20L12%2C6.99952461%20L11%2C6.99952461%20L11%2C7.99956118%20Z%20M13%2C16.9998903%20L14%2C16.9998903%20L14%2C15.9998537%20L13%2C15.9998537%20L13%2C16.9998903%20Z%20M11%2C16.9998903%20L12%2C16.9998903%20L12%2C15.9998537%20L11%2C15.9998537%20L11%2C16.9998903%20Z%20M13.18%2C14.884813%20L10.18%2C12.3847215%20C10.065%2C12.288718%2010%2C12.1487129%2010%2C11.9997075%20C10%2C11.851702%2010.065%2C11.7106969%2010.18%2C11.6156934%20L13.18%2C9.11560199%20L13.82%2C9.88463011%20L11.281%2C11.9997075%20L13.82%2C14.1157848%20L13.18%2C14.884813%20Z%22%20id%3D%22Amazon-API-Gateway_Icon_16_Squid%22%20fill%3D%22%23FFFFFF%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E", + "layout": [ + { + "h": 1, + "i": "row_rest", + "maxH": 1, + "minH": 1, + "minW": 12, + "moved": false, + "static": false, + "w": 12, + "x": 0, + "y": 0 + }, + { + "h": 6, + "i": "rest_requests", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 1 + }, + { + "h": 6, + "i": "rest_latency", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 1 + }, + { + "h": 6, + "i": "rest_5xx", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 7 + }, + { + "h": 6, + "i": "rest_integration_latency", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 7 + }, + { + "h": 6, + "i": "rest_cache_hit", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 13 + }, + { + "h": 6, + "i": "rest_cache_miss", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 13 + }, + { + "h": 1, + "i": "row_http", + "maxH": 1, + "minH": 1, + "minW": 12, + "moved": false, + "static": false, + "w": 12, + "x": 0, + "y": 19 + }, + { + "h": 6, + "i": "http_count", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 20 + }, + { + "h": 6, + "i": "http_4xx", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 20 + }, + { + "h": 6, + "i": "http_5xx", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 26 + }, + { + "h": 6, + "i": "http_latency", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 26 + }, + { + "h": 6, + "i": "http_data_processed", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 32 + }, + { + "h": 1, + "i": "row_websocket", + "maxH": 1, + "minH": 1, + "minW": 12, + "moved": false, + "static": false, + "w": 12, + "x": 0, + "y": 38 + }, + { + "h": 6, + "i": "ws_connect_count", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 39 + }, + { + "h": 6, + "i": "ws_message_count", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 39 + }, + { + "h": 6, + "i": "ws_client_error", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 45 + }, + { + "h": 6, + "i": "ws_execution_error", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 45 + }, + { + "h": 6, + "i": "ws_integration_error", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 51 + }, + { + "h": 1, + "i": "row_common", + "maxH": 1, + "minH": 1, + "minW": 12, + "moved": false, + "static": false, + "w": 12, + "x": 0, + "y": 57 + }, + { + "h": 6, + "i": "common_integration_latency", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 58 + } + ], + "panelMap": { + "row_rest": { + "collapsed": false, + "widgets": [ + { + "h": 6, + "i": "rest_requests", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 1 + }, + { + "h": 6, + "i": "rest_latency", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 1 + }, + { + "h": 6, + "i": "rest_5xx", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 7 + }, + { + "h": 6, + "i": "rest_integration_latency", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 7 + }, + { + "h": 6, + "i": "rest_cache_hit", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 13 + }, + { + "h": 6, + "i": "rest_cache_miss", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 13 + } + ] + }, + "row_http": { + "collapsed": false, + "widgets": [ + { + "h": 6, + "i": "http_count", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 20 + }, + { + "h": 6, + "i": "http_4xx", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 20 + }, + { + "h": 6, + "i": "http_5xx", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 26 + }, + { + "h": 6, + "i": "http_latency", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 26 + }, + { + "h": 6, + "i": "http_data_processed", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 32 + } + ] + }, + "row_websocket": { + "collapsed": false, + "widgets": [ + { + "h": 6, + "i": "ws_connect_count", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 39 + }, + { + "h": 6, + "i": "ws_message_count", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 39 + }, + { + "h": 6, + "i": "ws_client_error", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 45 + }, + { + "h": 6, + "i": "ws_execution_error", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 45 + }, + { + "h": 6, + "i": "ws_integration_error", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 51 + } + ] + }, + "row_common": { + "collapsed": false, + "widgets": [ + { + "h": 6, + "i": "common_integration_latency", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 58 + } + ] + } + }, + "tags": [], + "title": "API Gateway Overview (Multi-API)", + "uploadedGrafana": false, + "variables": { + "Account": { + "allSelected": false, + "customValue": "", + "description": "AWS Account", + "id": "93447e60-3b35-407c-a86c-97254f641628", + "key": "93447e60-3b35-407c-a86c-97254f641628", + "modificationUUID": "5e1f8a79-e9b5-4230-af48-f2c117805b31", + "multiSelect": false, + "name": "Account", + "order": 0, + "queryValue": "SELECT JSONExtractString(labels, 'cloud.account.id') as `cloud.account.id`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like '%aws_ApiGateway%'\nGROUP BY `cloud.account.id`\n\n", + "showALLOption": false, + "sort": "DISABLED", + "textboxValue": "", + "type": "QUERY" + }, + "Region": { + "allSelected": false, + "customValue": "", + "description": "AWS Region", + "id": "613f7913-ace1-4cf5-bb4c-df91878ec40e", + "key": "613f7913-ace1-4cf5-bb4c-df91878ec40e", + "modificationUUID": "bb86427a-e4da-4440-93d3-0f53e65214ed", + "multiSelect": false, + "name": "Region", + "order": 1, + "queryValue": "SELECT JSONExtractString(labels, 'cloud.region') as `cloud.region`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like '%aws_ApiGateway%'\n and JSONExtractString(labels, 'cloud.account.id') IN {{.Account}}\nGROUP BY `cloud.region`\n", + "showALLOption": false, + "sort": "DISABLED", + "textboxValue": "", + "type": "QUERY" + } + }, + "version": "v4", + "widgets": [ + { + "description": "", + "id": "row_rest", + "panelTypes": "row", + "title": "REST API Metrics" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The total number API requests in a given period.", + "fillSpans": false, + "id": "rest_requests", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_Count_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_Count_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "d3e9ea95", + "key": { + "dataType": "string", + "id": "ApiName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiName", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "81918ce2", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "114c7ff4", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiName", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiName}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "id": "rest-requests-query", + "queryType": "builder" + }, + + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Requests", + "yAxisUnit": "cpm" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The time between when API Gateway receives a request from a client and when it returns a response to the client.", + "fillSpans": false, + "id": "rest_latency", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_Latency_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_Latency_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "4b55a919", + "key": { + "dataType": "string", + "id": "ApiName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiName", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "3c67d1fc", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "e2a96f23", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiName", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiName}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "id": "rest-latency-query", + "queryType": "builder" + }, + + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Latency", + "yAxisUnit": "ms" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The number of server-side errors captured in a given period.", + "fillSpans": false, + "id": "rest_5xx", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_5XXError_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_5XXError_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "3e4a6042", + "key": { + "dataType": "string", + "id": "ApiName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiName", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "b3ebaf28", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "f2030d94", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiName", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiName}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "id": "rest-5xx-query", + "queryType": "builder" + }, + + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "5XX Errors", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The time between when API Gateway relays a request to the backend and when it receives a response from the backend.", + "fillSpans": false, + "id": "rest_integration_latency", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_IntegrationLatency_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_IntegrationLatency_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "1a3f91b4", + "key": { + "dataType": "string", + "id": "ApiName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiName", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "5f2f2892", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "960ee6d2", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiName", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiName}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "id": "rest-integ-latency-query", + "queryType": "builder" + }, + + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Integration Latency", + "yAxisUnit": "ms" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The number of requests served from the API cache in a given period.\n\nSee CacheHitCount at https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-metrics-and-dimensions.html for more details", + "fillSpans": false, + "id": "rest_cache_hit", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_CacheHitCount_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_CacheHitCount_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "8016ef52", + "key": { + "dataType": "string", + "id": "ApiName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiName", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "8ec09e30", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "d1622884", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiName", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiName}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "id": "rest-cache-hit-query", + "queryType": "builder" + }, + + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Cache Hits", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The number of requests served from the backend in a given period, when API caching is enabled.\n\nSee CacheMissCount at https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-metrics-and-dimensions.html for more details", + "fillSpans": false, + "id": "rest_cache_miss", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_CacheMissCount_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_CacheMissCount_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "6cbbb46e", + "key": { + "dataType": "string", + "id": "ApiName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiName", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "f02d484d", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "4d8ddc75", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiName", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiName}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "id": "rest-cache-miss-query", + "queryType": "builder" + }, + + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Cache Misses", + "yAxisUnit": "none" + }, + { + "description": "", + "id": "row_http", + "panelTypes": "row", + "title": "HTTP API Metrics" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The total number of HTTP API requests in a given period.", + "fillSpans": false, + "id": "http_count", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_Count_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_Count_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "http1", + "key": { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "http2", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "http3", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiId}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "id": "http-count-query", + "queryType": "builder" + }, + + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Requests", + "yAxisUnit": "cpm" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The number of client-side errors (4XX errors) in a given period.", + "fillSpans": false, + "id": "http_4xx", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_4xx_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_4xx_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "http4xx1", + "key": { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "http4xx2", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "http4xx3", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiId}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "id": "http-4xx-query", + "queryType": "builder" + }, + + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "4XX Errors", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The number of server-side errors (5XX errors) in a given period.", + "fillSpans": false, + "id": "http_5xx", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_5xx_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_5xx_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "http5xx1", + "key": { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "http5xx2", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "http5xx3", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiId}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "id": "http-5xx-query", + "queryType": "builder" + }, + + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "5XX Errors", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The total time between when API Gateway receives a request from a client and when it returns a response to the client.", + "fillSpans": false, + "id": "http_latency", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_Latency_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_Latency_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "httplat1", + "key": { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "httplat2", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "httplat3", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiId}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "id": "http-latency-query", + "queryType": "builder" + }, + + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Latency", + "yAxisUnit": "ms" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The amount of data processed in bytes for HTTP API requests.", + "fillSpans": false, + "id": "http_data_processed", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_DataProcessed_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_DataProcessed_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "httpdata1", + "key": { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "httpdata2", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "httpdata3", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiId}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "id": "http-data-query", + "queryType": "builder" + }, + + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Data Processed", + "yAxisUnit": "bytes" + }, + { + "description": "", + "id": "row_websocket", + "panelTypes": "row", + "title": "WebSocket API Metrics" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The number of messages sent to the $connect route integration.", + "fillSpans": false, + "id": "ws_connect_count", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_ConnectCount_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_ConnectCount_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "wscon1", + "key": { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "wscon2", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "wscon3", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiId}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "id": "ws-connect-query", + "queryType": "builder" + }, + + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Connect Count", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The number of messages sent to your WebSocket API.", + "fillSpans": false, + "id": "ws_message_count", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_MessageCount_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_MessageCount_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "wsmsg1", + "key": { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "wsmsg2", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "wsmsg3", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiId}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "id": "ws-message-query", + "queryType": "builder" + }, + + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Message Count", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The number of requests that return a 4XX response from API Gateway before the integration is invoked.", + "fillSpans": false, + "id": "ws_client_error", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_ClientError_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_ClientError_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "wscli1", + "key": { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "wscli2", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "wscli3", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiId}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "id": "ws-client-error-query", + "queryType": "builder" + }, + + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Client Errors (4XX)", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "Errors that occurred when calling the integration (5XX responses).", + "fillSpans": false, + "id": "ws_execution_error", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_ExecutionError_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_ExecutionError_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "wsexec1", + "key": { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "wsexec2", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "wsexec3", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiId}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "id": "ws-exec-error-query", + "queryType": "builder" + }, + + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Execution Errors (5XX)", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The number of requests that return a 4XX/5XX response from the integration.", + "fillSpans": false, + "id": "ws_integration_error", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_IntegrationError_sum--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_IntegrationError_sum", + "type": "Gauge" + }, + "aggregateOperator": "sum", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "wsint1", + "key": { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "wsint2", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "wsint3", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + } + ], + "having": [], + "legend": "{{ApiId}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "sum", + "timeAggregation": "sum" + } + ], + "queryFormulas": [] + }, + "id": "ws-integ-error-query", + "queryType": "builder" + }, + + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Integration Errors", + "yAxisUnit": "none" + }, + { + "description": "", + "id": "row_common", + "panelTypes": "row", + "title": "Common Metrics (HTTP & WebSocket APIs)" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "The time between when API Gateway relays a request to the backend and when it receives a response from the backend. This metric applies to both HTTP and WebSocket APIs.", + "fillSpans": false, + "id": "common_integration_latency", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ApiGateway_IntegrationLatency_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ApiGateway_IntegrationLatency_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "commonintlat1", + "key": { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + }, + "op": "exists", + "value": "" + }, + { + "id": "commonintlat2", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "commonintlat3", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "commonintlat4", + "key": { + "dataType": "string", + "id": "Stage--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "Stage", + "type": "tag" + }, + "op": "exists", + "value": "" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ApiId--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ApiId", + "type": "tag" + }, + { + "dataType": "string", + "id": "Stage--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "Stage", + "type": "tag" + } + ], + "having": [], + "legend": "{{Stage}} - {{ApiId}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "id": "common-integ-latency-query", + "queryType": "builder" + }, + + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Integration Latency", + "yAxisUnit": "ms" + } + ] +} diff --git a/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/api-gateway/icon.svg b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/api-gateway/icon.svg new file mode 100644 index 00000000000..e7cf9b72f00 --- /dev/null +++ b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/api-gateway/icon.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/api-gateway/integration.json b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/api-gateway/integration.json new file mode 100644 index 00000000000..b896cc68094 --- /dev/null +++ b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/api-gateway/integration.json @@ -0,0 +1,200 @@ +{ + "id": "api-gateway", + "title": "API Gateway", + "icon": "file://icon.svg", + "overview": "file://overview.md", + "supportedSignals": { + "metrics": true, + "logs": true + }, + "dataCollected": { + "metrics": [ + { + "name": "aws_ApiGateway_4XXError_count", + "unit": "Count", + "type": "Gauge" + }, + { + "name": "aws_ApiGateway_4XXError_max", + "unit": "Count", + "type": "Gauge" + }, + { + "name": "aws_ApiGateway_4XXError_min", + "unit": "Count", + "type": "Gauge" + }, + { + "name": "aws_ApiGateway_4XXError_sum", + "unit": "Count", + "type": "Gauge" + }, + { + "name": "aws_ApiGateway_5XXError_count", + "unit": "Count", + "type": "Gauge" + }, + { + "name": "aws_ApiGateway_5XXError_max", + "unit": "Count", + "type": "Gauge" + }, + { + "name": "aws_ApiGateway_5XXError_min", + "unit": "Count", + "type": "Gauge" + }, + { + "name": "aws_ApiGateway_5XXError_sum", + "unit": "Count", + "type": "Gauge" + }, + { + "name": "aws_ApiGateway_CacheHitCount_count", + "unit": "Count", + "type": "Gauge" + }, + { + "name": "aws_ApiGateway_CacheHitCount_max", + "unit": "Count", + "type": "Gauge" + }, + { + "name": "aws_ApiGateway_CacheHitCount_min", + "unit": "Count", + "type": "Gauge" + }, + { + "name": "aws_ApiGateway_CacheHitCount_sum", + "unit": "Count", + "type": "Gauge" + }, + { + "name": "aws_ApiGateway_CacheMissCount_count", + "unit": "Count", + "type": "Gauge" + }, + { + "name": "aws_ApiGateway_CacheMissCount_max", + "unit": "Count", + "type": "Gauge" + }, + { + "name": "aws_ApiGateway_CacheMissCount_min", + "unit": "Count", + "type": "Gauge" + }, + { + "name": "aws_ApiGateway_CacheMissCount_sum", + "unit": "Count", + "type": "Gauge" + }, + { + "name": "aws_ApiGateway_Count_count", + "unit": "Count", + "type": "Gauge" + }, + { + "name": "aws_ApiGateway_Count_max", + "unit": "Count", + "type": "Gauge" + }, + { + "name": "aws_ApiGateway_Count_min", + "unit": "Count", + "type": "Gauge" + }, + { + "name": "aws_ApiGateway_Count_sum", + "unit": "Count", + "type": "Gauge" + }, + { + "name": "aws_ApiGateway_IntegrationLatency_count", + "unit": "Milliseconds", + "type": "Gauge" + }, + { + "name": "aws_ApiGateway_IntegrationLatency_max", + "unit": "Milliseconds", + "type": "Gauge" + }, + { + "name": "aws_ApiGateway_IntegrationLatency_min", + "unit": "Milliseconds", + "type": "Gauge" + }, + { + "name": "aws_ApiGateway_IntegrationLatency_sum", + "unit": "Milliseconds", + "type": "Gauge" + }, + { + "name": "aws_ApiGateway_Latency_count", + "unit": "Milliseconds", + "type": "Gauge" + }, + { + "name": "aws_ApiGateway_Latency_max", + "unit": "Milliseconds", + "type": "Gauge" + }, + { + "name": "aws_ApiGateway_Latency_min", + "unit": "Milliseconds", + "type": "Gauge" + }, + { + "name": "aws_ApiGateway_Latency_sum", + "unit": "Milliseconds", + "type": "Gauge" + } + ], + "logs": [ + { + "name": "Account Id", + "path": "resources.cloud.account.id", + "type": "string" + }, + { + "name": "Log Group Name", + "path": "resources.aws.cloudwatch.log_group_name", + "type": "string" + }, + { + "name": "Log Stream Name", + "path": "resources.aws.cloudwatch.log_stream_name", + "type": "string" + } + ] + }, + "telemetryCollectionStrategy": { + "aws": { + "metrics": { + "cloudwatchMetricStreamFilters": [ + { + "Namespace": "AWS/ApiGateway" + } + ] + }, + "logs": { + "cloudwatchLogsSubscriptions": [ + { + "logGroupNamePrefix": "API-Gateway", + "filterPattern": "" + } + ] + } + } + }, + "assets": { + "dashboards": [ + { + "id": "overview", + "title": "API Gateway Overview", + "description": "Overview of API Gateway", + "definition": "file://assets/dashboards/overview.json" + } + ] + } +} diff --git a/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/api-gateway/overview.md b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/api-gateway/overview.md new file mode 100644 index 00000000000..47ad1802b6a --- /dev/null +++ b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/api-gateway/overview.md @@ -0,0 +1,3 @@ +### Monitor API Gateway with SigNoz + +Collect key API Gateway metrics and view them with an out of the box dashboard. diff --git a/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/dynamodb/assets/dashboards/overview.json b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/dynamodb/assets/dashboards/overview.json new file mode 100644 index 00000000000..4b64263d724 --- /dev/null +++ b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/dynamodb/assets/dashboards/overview.json @@ -0,0 +1,2657 @@ +{ + "description": "View DynamoDB metrics with an out-of-the-box dashboard.", + "image":"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iODBweCIgaGVpZ2h0PSI4MHB4IiB2aWV3Qm94PSIwIDAgODAgODAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDY0ICg5MzUzNykgLSBodHRwczovL3NrZXRjaC5jb20gLS0+CiAgICA8dGl0bGU+SWNvbi1BcmNoaXRlY3R1cmUvNjQvQXJjaF9BbWF6b24tRHluYW1vREJfNjQ8L3RpdGxlPgogICAgPGRlc2M+Q3JlYXRlZCB3aXRoIFNrZXRjaC48L2Rlc2M+CiAgICA8ZGVmcz4KICAgICAgICA8bGluZWFyR3JhZGllbnQgeDE9IjAlIiB5MT0iMTAwJSIgeDI9IjEwMCUiIHkyPSIwJSIgaWQ9ImxpbmVhckdyYWRpZW50LTEiPgogICAgICAgICAgICA8c3RvcCBzdG9wLWNvbG9yPSIjMkUyN0FEIiBvZmZzZXQ9IjAlIj48L3N0b3A+CiAgICAgICAgICAgIDxzdG9wIHN0b3AtY29sb3I9IiM1MjdGRkYiIG9mZnNldD0iMTAwJSI+PC9zdG9wPgogICAgICAgIDwvbGluZWFyR3JhZGllbnQ+CiAgICA8L2RlZnM+CiAgICA8ZyBpZD0iSWNvbi1BcmNoaXRlY3R1cmUvNjQvQXJjaF9BbWF6b24tRHluYW1vREJfNjQiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPgogICAgICAgIDxnIGlkPSJJY29uLUFyY2hpdGVjdHVyZS1CRy82NC9EYXRhYmFzZSIgZmlsbD0idXJsKCNsaW5lYXJHcmFkaWVudC0xKSI+CiAgICAgICAgICAgIDxyZWN0IGlkPSJSZWN0YW5nbGUiIHg9IjAiIHk9IjAiIHdpZHRoPSI4MCIgaGVpZ2h0PSI4MCI+PC9yZWN0PgogICAgICAgIDwvZz4KICAgICAgICA8cGF0aCBkPSJNNTIuMDg1OTUyNSw1NC44NTAyNTA2IEM0OC43NDc5NTY5LDU3LjU0OTAzMzggNDEuNzQ0OTY2MSw1OC45NzUyOTI3IDM1LjA0Mzk3NDksNTguOTc1MjkyNyBDMjguMzQxOTgzOCw1OC45NzUyOTI3IDIxLjMzNjk5Myw1Ny41NDgwNDIgMTcuOTk5OTk3NCw1NC44NDkyNTg4IEwxNy45OTk5OTc0LDYwLjI4NDUxNSBMMTguMDAwOTk3NCw2MC4yODQ1MTUgQzE4LjAwMDk5NzQsNjIuOTk1MjAwMiAyNC45OTk5OTc0LDY2LjAxNjMyOTkgMzUuMDQzOTc0OSw2Ni4wMTYzMjk5IEM0NS4wNzk5NjE3LDY2LjAxNjMyOTkgNTIuMDc0OTUyNSw2Mi45OTkxNjc2IDUyLjA4NTk1MjUsNjAuMjkwNDY2IEw1Mi4wODU5NTI1LDU0Ljg1MDI1MDYgWiBNNTIuMDg2OTUyNSw0NC41MjIyNzIgTDU0LjA4Njk0OTksNDQuNTExMzYxOCBMNTQuMDg2OTQ5OSw0NC41MjIyNzIgQzU0LjA4Njk0OTksNDUuNzMwMzI3MSA1My40ODE5NTA3LDQ2Ljg1ODA0MzYgNTIuMzAzOTUyMiw0Ny44OTA1NDM5IEM1My43MzE5NTAzLDQ5LjE0NzE5OSA1NC4wODY5NDk5LDUwLjM4MDA0OTkgNTQuMDg2OTQ5OSw1MS4yNTc4MjQgQzU0LjA4Njk0OTksNTEuMjYzNzc1IDU0LjA4NTk0OTksNTEuMjY4NzM0MiA1NC4wODU5NDk5LDUxLjI3NDY4NTIgTDU0LjA4NTk0OTksNjAuMjg0NTE1IEw1NC4wODY5NDk5LDYwLjI4NDUxNSBDNTQuMDg2OTQ5OSw2NS4yOTUyNjU4IDQ0LjI3NDk2MjgsNjggMzUuMDQzOTc0OSw2OCBDMjUuODM0OTg3MSw2OCAxNi4wNDk5OTk5LDY1LjMwNzE2NzggMTYuMDAzLDYwLjMxOTIyOTIgQzE2LjAwMyw2MC4zMTQyNyAxNiw2MC4zMDkzMTA5IDE2LDYwLjMwNDM1MTcgTDE2LDUxLjI1NDg0ODUgQzE2LDUxLjI1Mjg2NDggMTYuMDAyLDUxLjI0OTg4OTMgMTYuMDAyLDUxLjI0NjkxMzggQzE2LjAwNSw1MC4zNjkxMzk4IDE2LjM2MDk5OTUsNDkuMTQxMjQ3OSAxNy43ODY5OTc2LDQ3Ljg4NzU2ODQgQzE2LjM2OTk5OTUsNDYuNjM1ODcyNSAxNi4wMSw0NS40MTQ5MjM2IDE2LjAwMSw0NC41NDQwOTI0IEwxNi4wMDIsNDQuNTQ0MDkyNCBDMTYuMDAyLDQ0LjU0MDEyNSAxNiw0NC41MzcxNDk1IDE2LDQ0LjUzMzE4MjIgTDE2LDM1LjQ4MzY3OSBDMTYsMzUuNDgwNzAzNSAxNi4wMDIsMzUuNDc3NzI4IDE2LjAwMiwzNS40NzQ3NTI1IEMxNi4wMDUsMzQuNTk2OTc4NCAxNi4zNjE5OTk1LDMzLjM2OTA4NjYgMTcuNzg3OTk3NiwzMi4xMTczOTA4IEMxNi4zNjk5OTk1LDMwLjg2NDcwMzEgMTYuMDEsMjkuNjQyNzYyMyAxNi4wMDEsMjguNzcyOTIyOSBMMTYuMDAyLDI4Ljc3MjkyMjkgQzE2LjAwMiwyOC43Njg5NTU2IDE2LDI4Ljc2NDk4ODIgMTYsMjguNzYxMDIwOSBMMTYsMTkuNzEyNTA5NSBDMTYsMTkuNzA5NTM0IDE2LjAwMiwxOS43MDY1NTg1IDE2LjAwMiwxOS43MDM1ODMgQzE2LjAxOSwxNC42OTk3NzUxIDI1LjgxOTk4NzEsMTIgMzUuMDQzOTc0OSwxMiBDNDAuMjU0OTY4MSwxMiA0NS4yNjA5NjE1LDEyLjgyODE4MjMgNDguNzc3OTU2OSwxNC4yNzIyOTQxIEw0OC4wMTI5NTc5LDE2LjEwNTIwNTQgQzQ0LjcyOTk2MjIsMTQuNzU3MzAxNSA0MC4wMDI5Njg0LDEzLjk4MzY3MDEgMzUuMDQzOTc0OSwxMy45ODM2NzAxIEMyNC45OTk5ODgyLDEzLjk4MzY3MDEgMTguMDAwOTk3NCwxNy4wMDQ3OTk4IDE4LjAwMDk5NzQsMTkuNzE3NDY4NyBDMTguMDAwOTk3NCwyMi40MjkxNDU4IDI0Ljk5OTk4ODIsMjUuNDUwMjc1NCAzNS4wNDM5NzQ5LDI1LjQ1MDI3NTQgQzM1LjMxNDk3NDYsMjUuNDUzMjUwOSAzNS41Nzk5NzQyLDI1LjQ1MDI3NTQgMzUuODQ3OTczOSwyNS40NDAzNTcxIEwzNS45MzE5NzM4LDI3LjQyMjA0MzUgQzM1LjYzNTk3NDIsMjcuNDMzOTQ1NiAzNS4zMzk5NzQ1LDI3LjQzMzk0NTYgMzUuMDQzOTc0OSwyNy40MzM5NDU2IEMyOC4zNDE5ODM4LDI3LjQzMzk0NTYgMjEuMzM2OTkzLDI2LjAwNjY5NDkgMTgsMjMuMzA3OTExNyBMMTgsMjguNzQwMTkyMyBMMTguMDAwOTk3NCwyOC43NDAxOTIzIEwxOC4wMDA5OTc0LDI4Ljc2MzAwNDYgQzE4LjAxMDk5NzQsMjkuODAzNDM5NSAxOS4wNzc5OTU5LDMwLjcxMTk2MDUgMTkuOTcxOTk0OCwzMS4yODkyMDg1IEMyMi42NjE5OTEyLDMzLjAwNDA5MTMgMjcuNDgxOTg0OSwzNC4xNzU0NDg1IDMyLjg1Njk3NzgsMzQuNDE4NDQ4MSBMMzIuNzY1OTc3OSwzNi40MDAxMzQ2IEMyNy4zMjA5ODUxLDM2LjE1MzE2NzcgMjIuNTUyOTkxNCwzNS4wMjM0Njc1IDE5LjQ4Mzk5NTQsMzMuMjkxNzIzNSBDMTguNzI3OTk2NCwzMy44NTcwNjk1IDE4LjAwMDk5NzQsMzQuNjIxNzc0MyAxOC4wMDA5OTc0LDM1LjQ4ODYzODIgQzE4LjAwMDk5NzQsMzguMjAwMzE1MyAyNC45OTk5ODgyLDQxLjIyMTQ0NDkgMzUuMDQzOTc0OSw0MS4yMjE0NDQ5IEMzNi4wMjg5NzM2LDQxLjIyMTQ0NDkgMzcuMDA2OTcyMyw0MS4xODg3MTQzIDM3Ljk1MTk3MTEsNDEuMTIzMjUzMiBMMzguMDkwOTcwOSw0My4xMDE5NjQyIEMzNy4xMDA5NzIyLDQzLjE3MDQwMDggMzYuMDc0OTczNiw0My4yMDUxMTUgMzUuMDQzOTc0OSw0My4yMDUxMTUgQzI4LjM0MTk4MzgsNDMuMjA1MTE1IDIxLjMzNjk5Myw0MS43Nzc4NjQ0IDE4LDM5LjA3OTA4MTEgTDE4LDQ0LjUxMTM2MTggTDE4LjAwMDk5NzQsNDQuNTExMzYxOCBDMTguMDEwOTk3NCw0NS41NzQ2MDkgMTkuMDc3OTk1OSw0Ni40ODIxMzgxIDE5Ljk3MTk5NDgsNDcuMDYwMzc4IEMyMy4wNDc5OTA3LDQ5LjAyMzIxOTYgMjguODIzOTgzMSw1MC4yNDUxNjA0IDM1LjA0Mzk3NDksNTAuMjQ1MTYwNCBMMzUuNDgzOTc0NCw1MC4yNDUxNjA0IEwzNS40ODM5NzQ0LDUyLjIyODgzMDUgTDM1LjA0Mzk3NDksNTIuMjI4ODMwNSBDMjguNzI0OTgzMiw1Mi4yMjg4MzA1IDIyLjk4MTk5MDgsNTEuMDU1NDg5NiAxOS40Njk5OTU0LDQ5LjA3MjgxMTMgQzE4LjcxNzk5NjQsNDkuNjM3MTY1NSAxOC4wMDA5OTc0LDUwLjM5NzkwMyAxOC4wMDA5OTc0LDUxLjI1NzgyNCBDMTguMDAwOTk3NCw1My45Njk1MDExIDI0Ljk5OTk4ODIsNTYuOTkxNjIyNSAzNS4wNDM5NzQ5LDU2Ljk5MTYyMjUgQzQ1LjA3OTk2MTcsNTYuOTkxNjIyNSA1Mi4wNzQ5NTI1LDUzLjk3NDQ2MDIgNTIuMDg1OTUyNSw1MS4yNjQ3NjY4IEw1Mi4wODU5NTI1LDUxLjI1NDg0ODUgTDUyLjA4NTk1MjUsNTEuMjUzODU2NiBDNTIuMDgzOTUyNSw1MC4zOTE5NTIgNTEuMzYzOTUzNCw0OS42MzEyMTQ1IDUwLjYwOTk1NDQsNDkuMDY2ODYwMyBDNTAuMTIxOTU1MSw0OS4zNDM1ODIzIDQ5LjU5ODk1NTgsNDkuNjEwMzg1OSA0OS4wMDM5NTY2LDQ5Ljg1NTM2OTIgTDQ4LjIzNzk1NzYsNDguMDIyNDU4IEM0OC45NjM5NTY2LDQ3LjcyMzkxNTYgNDkuNTkzOTU1OCw0Ny40MDE1NjkyIDUwLjExMDk1NTEsNDcuMDYyMzYxNiBDNTEuMDEyOTUzOSw0Ni40NzQyMDM0IDUyLjA4Njk1MjUsNDUuNTU0NzcyMyA1Mi4wODY5NTI1LDQ0LjUyMjI3MiBMNTIuMDg2OTUyNSw0NC41MjIyNzIgWiBNNjAuNjUyOTQxMiwzMC4wMTY2ODQxIEw1NS4wNDg5NDg2LDMwLjAxNjY4NDEgQzU0LjcxNzk0OSwzMC4wMTY2ODQxIDU0LjQwNjk0OTQsMjkuODU0MDIzMSA1NC4yMjE5NDk3LDI5LjU4MjI2MDMgQzU0LjAzNDk0OTksMjkuMzEwNDk3NSA1My45OTY5NSwyOC45NjQzNDcxIDU0LjExODk0OTgsMjguNjU5ODUzNyBMNTcuNTI3OTQ1MywyMC4xMzgwMDY4IEw0NC42MTg5NzAyLDIwLjEzODAwNjggTDM4LjYxODk3MDIsMzIuMDQwMDI3NiBMNDUuMDAwOTYxOCwzMi4wNDAwMjc2IEM0NS4zMTk5NjE0LDMyLjA0MDAyNzYgNDUuNjE5OTYxLDMyLjE5MTc3ODQgNDUuODA4OTYwOCwzMi40NDY2OCBDNDUuOTk1OTYwNSwzMi43MDI1NzM1IDQ2LjA1MDk2MDQsMzMuMDMwODcwOSA0NS45NTM5NjA2LDMzLjMzMzM4MDYgTDQwLjI1Nzk2ODEsNTEuMDg5MjEyIEw2MC42NTI5NDEyLDMwLjAxNjY4NDEgWiBNNjMuNzIxOTM3MiwyOS43MTIxOTA3IEwzOC43MjI5NzAxLDU1LjUzOTU3NiBDMzguNTI3OTcwMyw1NS43Mzk5MjY3IDM4LjI2NTk3MDcsNTUuODQ0MDY5NCAzOC4wMDA5NzEsNTUuODQ0MDY5NCBDMzcuODI0OTcxMyw1NS44NDQwNjk0IDM3LjY0Nzk3MTUsNTUuNzk5NDM2OCAzNy40ODk5NzE3LDU1LjcwNTIxMjQgQzM3LjA4OTk3MjIsNTUuNDY5MTU1NyAzNi45MDY5NzI1LDU0Ljk5MjA4MyAzNy4wNDc5NzIzLDU0LjU1MTcwODMgTDQzLjYzMzk2MzYsMzQuMDIzNjk3OCBMMzcuMDAwOTcyNCwzNC4wMjM2OTc4IEMzNi42NTM5NzI4LDM0LjAyMzY5NzggMzYuMzMyOTczMiwzMy44NDYxNTkzIDM2LjE0OTk3MzUsMzMuNTUzNTY3OSBDMzUuOTY3OTczNywzMy4yNjA5NzY2IDM1Ljk1MDk3MzcsMzIuODk1OTgxMyAzNi4xMDY5NzM1LDMyLjU4ODUxMjQgTDQzLjEwNjk2NDMsMTguNzAyODIxNCBDNDMuMjc1OTY0MSwxOC4zNjY1ODkzIDQzLjYyMTk2MzYsMTguMTU0MzM2NiA0NC4wMDA5NjMxLDE4LjE1NDMzNjYgTDU5LjAwMDk0MzQsMTguMTU0MzM2NiBDNTkuMzMxOTQzLDE4LjE1NDMzNjYgNTkuNjQyOTQyNSwxOC4zMTc5ODk0IDU5LjgyNzk0MjMsMTguNTg4NzYwNCBDNjAuMDE0OTQyMSwxOC44NjE1MTUgNjAuMDUyOTQyLDE5LjIwNjY3MzYgNTkuOTMwOTQyMiwxOS41MTIxNTg4IEw1Ni41MjE5NDY3LDI4LjAzMzAxMzkgTDYyLjk5OTkzODEsMjguMDMzMDEzOSBDNjMuMzk5OTM3NiwyOC4wMzMwMTM5IDYzLjc2MjkzNzEsMjguMjcxMDU0NCA2My45MTk5MzY5LDI4LjYzNjA0OTcgQzY0LjA3NjkzNjcsMjkuMDAyMDM2OCA2My45OTg5MzY4LDI5LjQyNTU1MDQgNjMuNzIxOTM3MiwyOS43MTIxOTA3IEw2My43MjE5MzcyLDI5LjcxMjE5MDcgWiBNMTkuNDU0OTk1NSw2MC42NzQzMDYyIEMyMC44NzE5OTM2LDYxLjQ3MjczMzQgMjIuNjU1OTkxMiw2Mi4xNDQyMDU3IDI0Ljc1Njk4ODUsNjIuNjY3ODk0NyBMMjUuMjQ0OTg3OCw2MC43NDM3MzQ2IEMyMy4zNDU5OTAzLDYwLjI3MDYyOTMgMjEuNjg1OTkyNSw1OS42NDk3NDA1IDIwLjQ0Mjk5NDIsNTguOTQ5NTA1IEwxOS40NTQ5OTU1LDYwLjY3NDMwNjIgWiBNMjQuNzU2OTg4NSw0Ni43OTg1MzM1IEwyNS4yNDQ5ODc4LDQ0Ljg3NTM2NTMgQzIzLjM0NTk5MDMsNDQuNDAxMjY4MSAyMS42ODU5OTI1LDQzLjc4MDM3OTQgMjAuNDQyOTk0Miw0My4wODAxNDM4IEwxOS40NTQ5OTU1LDQ0LjgwNDk0NSBDMjAuODcxOTkzNiw0NS42MDMzNzIyIDIyLjY1NDk5MTIsNDYuMjc0ODQ0NiAyNC43NTY5ODg1LDQ2Ljc5ODUzMzUgTDI0Ljc1Njk4ODUsNDYuNzk4NTMzNSBaIE0xOS40NTQ5OTU1LDI4LjkzNTU4MzkgTDIwLjQ0Mjk5NDIsMjcuMjEwNzgyNyBDMjEuNjgzOTkyNSwyNy45MTEwMTgyIDIzLjM0NDk5MDMsMjguNTMwOTE1MSAyNS4yNDQ5ODc4LDI5LjAwNjAwNDEgTDI0Ljc1Njk4ODUsMzAuOTI5MTcyMyBDMjIuNjUyOTkxMiwzMC40MDQ0OTE2IDIwLjg2OTk5MzYsMjkuNzMzMDE5MyAxOS40NTQ5OTU1LDI4LjkzNTU4MzkgTDE5LjQ1NDk5NTUsMjguOTM1NTgzOSBaIiBpZD0iQW1hem9uLUR5bmFtb0RCX0ljb25fNjRfU3F1aWQiIGZpbGw9IiNGRkZGRkYiPjwvcGF0aD4KICAgIDwvZz4KPC9zdmc+", + "layout": [ + { + "h": 6, + "i": "9e1d91ec-fb66-4cff-b5c5-282270ebffb5", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 0 + }, + { + "h": 6, + "i": "9a2daf2e-39bc-445d-947f-617c27fadd0f", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 0 + }, + { + "h": 6, + "i": "5b50997d-3bca-466a-bdeb-841b2e49fd65", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 6 + }, + { + "h": 6, + "i": "889c36ab-4d0c-4328-9c3c-6558aad6be89", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 6 + }, + { + "h": 6, + "i": "0c3b97fe-56e0-4ce6-99f4-fd1cbd24f93e", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 12 + }, + { + "h": 6, + "i": "70980d38-ee3c-47be-9520-e371df3b021a", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 12 + }, + { + "h": 6, + "i": "fe1b71b5-1a3f-41c0-b6c2-46bf934787ad", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 18 + }, + { + "h": 6, + "i": "cc0938a5-af82-4bd8-b10e-67eabe717ee0", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 18 + }, + { + "h": 6, + "i": "4bb63c27-5eb4-4904-9947-42ffce15e92e", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 24 + }, + { + "h": 6, + "i": "5ffbe527-8cf3-4ed8-ac2d-8739fa7fa9af", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 24 + }, + { + "h": 6, + "i": "a02f64ac-e73e-4d4c-a26b-fcfc4265c148", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 30 + }, + { + "h": 6, + "i": "014e377d-b7c1-4469-a137-be34d7748f31", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 30 + }, + { + "h": 6, + "i": "b1b75926-7308-43b3-bcad-60f369715f0b", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 36 + }, + { + "h": 6, + "i": "90f4d19d-8785-4a7a-97cf-c967108e1487", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 36 + }, + { + "h": 6, + "i": "5412cdad-174b-462b-916e-4e3de477446b", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 42 + } + ], + "panelMap": {}, + "tags": [], + "title": "DynamoDB Overview", + "uploadedGrafana": false, + "variables": { + "1f7a94df-9735-4bfa-a1b8-dca8ac29f945": { + "allSelected": false, + "customValue": "", + "description": "Account Region", + "id": "1f7a94df-9735-4bfa-a1b8-dca8ac29f945", + "key": "1f7a94df-9735-4bfa-a1b8-dca8ac29f945", + "modificationUUID": "8ef772a1-7df9-46a2-84e7-ab0c0bfc6886", + "multiSelect": false, + "name": "Region", + "order": 1, + "queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud_region') AS region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name like '%aws_DynamoDB%' AND JSONExtractString(labels, 'cloud_account_id') IN {{.Account}} GROUP BY region", + "showALLOption": false, + "sort": "DISABLED", + "textboxValue": "", + "type": "QUERY" + }, + "93ee15bf-baab-4abf-8828-fe6e75518417": { + "allSelected": false, + "customValue": "", + "description": "AWS Account ID", + "id": "93ee15bf-baab-4abf-8828-fe6e75518417", + "key": "93ee15bf-baab-4abf-8828-fe6e75518417", + "modificationUUID": "409e6a7e-1ec1-4611-8624-492a3aac6ca0", + "multiSelect": false, + "name": "Account", + "order": 0, + "queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud_account_id') AS cloud_account_id\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name like '%aws_DynamoDB%' GROUP BY cloud_account_id", + "showALLOption": false, + "sort": "ASC", + "textboxValue": "", + "type": "QUERY" + }, + "fd28f0e0-d4ec-4bcd-9c45-32395cb0c55b": { + "allSelected": true, + "customValue": "", + "description": "DynamoDB Tables", + "id": "fd28f0e0-d4ec-4bcd-9c45-32395cb0c55b", + "modificationUUID": "8ebb9032-7e56-4981-8036-efdfc413f8a8", + "multiSelect": true, + "name": "Table", + "order": 2, + "queryValue": "SELECT DISTINCT JSONExtractString(labels, 'TableName') AS table FROM signoz_metrics.distributed_time_series_v4_1day WHERE metric_name like '%aws_DynamoDB%' AND JSONExtractString(labels, 'cloud_account_id') IN {{.Account}} AND JSONExtractString(labels, 'cloud_region') IN {{.Region}} and table != '' GROUP BY table\n", + "showALLOption": true, + "sort": "ASC", + "textboxValue": "", + "type": "QUERY" + } + }, + "version": "v4", + "widgets": [ + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "9e1d91ec-fb66-4cff-b5c5-282270ebffb5", + "isLogScale": false, + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_DynamoDB_AccountMaxReads_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_DynamoDB_AccountMaxReads_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "fc55895c", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "8b3f3e0b", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [], + "having": [], + "legend": "", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "4fdb1c6c-8c7f-4f8b-a468-9326c811981a", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Account Max Reads", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "5b50997d-3bca-466a-bdeb-841b2e49fd65", + "isLogScale": false, + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_DynamoDB_AccountMaxTableLevelReads_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_DynamoDB_AccountMaxTableLevelReads_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "f7b176f8", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "9a023ab7", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [], + "having": [], + "legend": "", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "310efa3b-d68a-4630-b279-bcbc22ddbefb", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Account Max Table Level Reads", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "889c36ab-4d0c-4328-9c3c-6558aad6be89", + "isLogScale": false, + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_DynamoDB_AccountMaxTableLevelWrites_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_DynamoDB_AccountMaxTableLevelWrites_max", + "type": "Gauge" + }, + "aggregateOperator": "avg", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "ec5ebf95", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "5b2fb00e", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [], + "having": [], + "legend": "", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "avg" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "473de955-bc5c-4a66-aa8d-2e37502c5643", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Account Max Table Level Writes", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "9a2daf2e-39bc-445d-947f-617c27fadd0f", + "isLogScale": false, + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_DynamoDB_AccountMaxWrites_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_DynamoDB_AccountMaxWrites_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "3815cf09", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "a783bd91", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [], + "having": [], + "legend": "", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "avg", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "1115aaa1-fdb0-47a1-af79-8c6d439747d4", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Account Max Writes", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "0c3b97fe-56e0-4ce6-99f4-fd1cbd24f93e", + "isLogScale": false, + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_DynamoDB_AccountProvisionedReadCapacityUtilization_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_DynamoDB_AccountProvisionedReadCapacityUtilization_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "edcbcb83", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "224766cb", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [], + "having": [], + "legend": "", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "d42bc3cd-f457-42eb-936e-c931b0c77f61", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Account Provisioned Read Capacity", + "yAxisUnit": "percent" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "70980d38-ee3c-47be-9520-e371df3b021a", + "isLogScale": false, + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_DynamoDB_AccountProvisionedWriteCapacityUtilization_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_DynamoDB_AccountProvisionedWriteCapacityUtilization_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "c237482a", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "e3a117d5", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [], + "having": [], + "legend": "", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "d06d2f3d-8878-4c53-a8f1-10024091887a", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Account Provisioned Write Capacity", + "yAxisUnit": "percent" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "fe1b71b5-1a3f-41c0-b6c2-46bf934787ad", + "isLogScale": false, + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_DynamoDB_ConsumedReadCapacityUnits_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_DynamoDB_ConsumedReadCapacityUnits_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "b867513b", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "9c10cbaa", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "4ff7fb7c", + "key": { + "dataType": "string", + "id": "TableName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "TableName", + "type": "tag" + }, + "op": "in", + "value": [ + "$Table" + ] + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "TableName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "TableName", + "type": "tag" + } + ], + "having": [], + "legend": "{{TableName}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "32c9f178-073c-4d1f-8193-76f804776df0", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Consumed Read Capacity", + "yAxisUnit": "percent" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "cc0938a5-af82-4bd8-b10e-67eabe717ee0", + "isLogScale": false, + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_DynamoDB_ConsumedWriteCapacityUnits_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_DynamoDB_ConsumedWriteCapacityUnits_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "7e2aa806", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "dd49e062", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "e7ada865", + "key": { + "dataType": "string", + "id": "TableName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "TableName", + "type": "tag" + }, + "op": "in", + "value": [ + "$Table" + ] + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "TableName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "TableName", + "type": "tag" + } + ], + "having": [], + "legend": "{{TableName}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "40397368-92df-42b9-b0e6-0e7dc7984bc4", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Consumed Write Capacity", + "yAxisUnit": "percent" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "4bb63c27-5eb4-4904-9947-42ffce15e92e", + "isLogScale": false, + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_DynamoDB_MaxProvisionedTableReadCapacityUtilization_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_DynamoDB_MaxProvisionedTableReadCapacityUtilization_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "b3e029fa", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "e6764d50", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [], + "having": [], + "legend": "", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "6a33d44a-a337-422f-a964-89b88804343f", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Max Provisioned Table Read Capacity", + "yAxisUnit": "percent" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "5ffbe527-8cf3-4ed8-ac2d-8739fa7fa9af", + "isLogScale": false, + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_DynamoDB_MaxProvisionedTableWriteCapacityUtilization_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_DynamoDB_MaxProvisionedTableWriteCapacityUtilization_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "80ba9142", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "9c802cf0", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [], + "having": [], + "legend": "", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "a98b7d13-63d3-46cf-b4e7-686b3be7d9f9", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Max Provisioned Table Write Capacity", + "yAxisUnit": "percent" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "a02f64ac-e73e-4d4c-a26b-fcfc4265c148", + "isLogScale": false, + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_DynamoDB_ReturnedItemCount_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_DynamoDB_ReturnedItemCount_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "db6edb77", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "8b86de4a", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "a8d39d03", + "key": { + "dataType": "string", + "id": "TableName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "TableName", + "type": "tag" + }, + "op": "in", + "value": [ + "$Table" + ] + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "TableName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "TableName", + "type": "tag" + } + ], + "having": [], + "legend": "{{TableName}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "6322f225-471d-43a2-b13e-f2312c1a7b57", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Returned Item Count", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "014e377d-b7c1-4469-a137-be34d7748f31", + "isLogScale": false, + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_DynamoDB_SuccessfulRequestLatency_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_DynamoDB_SuccessfulRequestLatency_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "93bef7f0", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "4a293ec8", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "2e2286c6", + "key": { + "dataType": "string", + "id": "TableName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "TableName", + "type": "tag" + }, + "op": "in", + "value": [ + "$Table" + ] + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "TableName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "TableName", + "type": "tag" + } + ], + "having": [], + "legend": "{{TableName}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "6ad1cbfe-9581-4d99-a14e-50bc5fef699f", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Successful Request Latency", + "yAxisUnit": "ms" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "b1b75926-7308-43b3-bcad-60f369715f0b", + "isLogScale": false, + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_DynamoDB_ThrottledRequests_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_DynamoDB_ThrottledRequests_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "28fcd3cd", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "619578e5", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "a6bc481e", + "key": { + "dataType": "string", + "id": "TableName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "TableName", + "type": "tag" + }, + "op": "in", + "value": [ + "$Table" + ] + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "TableName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "TableName", + "type": "tag" + } + ], + "having": [], + "legend": "{{TableName}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "fd358cf0-a0b0-4106-a89c-a5196297c23b", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Max Throttled Requests", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "5412cdad-174b-462b-916e-4e3de477446b", + "isLogScale": false, + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_DynamoDB_UserErrors_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_DynamoDB_UserErrors_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "5a060b5e", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "3a1cb5ff", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [], + "having": [], + "legend": "", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "17db2e6d-d9dc-4568-85ea-ea4b373dfc5e", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "User Errors", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "90f4d19d-8785-4a7a-97cf-c967108e1487", + "isLogScale": false, + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_DynamoDB_WriteThrottleEvents_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_DynamoDB_WriteThrottleEvents_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "58bc06b3", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "d6d7a8fb", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [], + "having": [], + "legend": "", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "713c6c70-3a62-4b67-8a67-7917ca9d4fbf", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Max Write Throttle Events", + "yAxisUnit": "none" + } + ] +} \ No newline at end of file diff --git a/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/dynamodb/assets/dashboards/overview.png b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/dynamodb/assets/dashboards/overview.png new file mode 100644 index 0000000000000000000000000000000000000000..18a5f314996283d03754624c221eb6cd790b3751 GIT binary patch literal 253551 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솯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 aU9)a&zS*t)+;89?HD%4;vu{3p`hNhEH*~Q8 literal 0 HcmV?d00001 diff --git a/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/dynamodb/assets/dashboards/overview_dot.json b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/dynamodb/assets/dashboards/overview_dot.json new file mode 100644 index 00000000000..8d290596600 --- /dev/null +++ b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/dynamodb/assets/dashboards/overview_dot.json @@ -0,0 +1,2657 @@ +{ + "description": "View DynamoDB metrics with an out-of-the-box dashboard.", + "image":"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iODBweCIgaGVpZ2h0PSI4MHB4IiB2aWV3Qm94PSIwIDAgODAgODAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDY0ICg5MzUzNykgLSBodHRwczovL3NrZXRjaC5jb20gLS0+CiAgICA8dGl0bGU+SWNvbi1BcmNoaXRlY3R1cmUvNjQvQXJjaF9BbWF6b24tRHluYW1vREJfNjQ8L3RpdGxlPgogICAgPGRlc2M+Q3JlYXRlZCB3aXRoIFNrZXRjaC48L2Rlc2M+CiAgICA8ZGVmcz4KICAgICAgICA8bGluZWFyR3JhZGllbnQgeDE9IjAlIiB5MT0iMTAwJSIgeDI9IjEwMCUiIHkyPSIwJSIgaWQ9ImxpbmVhckdyYWRpZW50LTEiPgogICAgICAgICAgICA8c3RvcCBzdG9wLWNvbG9yPSIjMkUyN0FEIiBvZmZzZXQ9IjAlIj48L3N0b3A+CiAgICAgICAgICAgIDxzdG9wIHN0b3AtY29sb3I9IiM1MjdGRkYiIG9mZnNldD0iMTAwJSI+PC9zdG9wPgogICAgICAgIDwvbGluZWFyR3JhZGllbnQ+CiAgICA8L2RlZnM+CiAgICA8ZyBpZD0iSWNvbi1BcmNoaXRlY3R1cmUvNjQvQXJjaF9BbWF6b24tRHluYW1vREJfNjQiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPgogICAgICAgIDxnIGlkPSJJY29uLUFyY2hpdGVjdHVyZS1CRy82NC9EYXRhYmFzZSIgZmlsbD0idXJsKCNsaW5lYXJHcmFkaWVudC0xKSI+CiAgICAgICAgICAgIDxyZWN0IGlkPSJSZWN0YW5nbGUiIHg9IjAiIHk9IjAiIHdpZHRoPSI4MCIgaGVpZ2h0PSI4MCI+PC9yZWN0PgogICAgICAgIDwvZz4KICAgICAgICA8cGF0aCBkPSJNNTIuMDg1OTUyNSw1NC44NTAyNTA2IEM0OC43NDc5NTY5LDU3LjU0OTAzMzggNDEuNzQ0OTY2MSw1OC45NzUyOTI3IDM1LjA0Mzk3NDksNTguOTc1MjkyNyBDMjguMzQxOTgzOCw1OC45NzUyOTI3IDIxLjMzNjk5Myw1Ny41NDgwNDIgMTcuOTk5OTk3NCw1NC44NDkyNTg4IEwxNy45OTk5OTc0LDYwLjI4NDUxNSBMMTguMDAwOTk3NCw2MC4yODQ1MTUgQzE4LjAwMDk5NzQsNjIuOTk1MjAwMiAyNC45OTk5OTc0LDY2LjAxNjMyOTkgMzUuMDQzOTc0OSw2Ni4wMTYzMjk5IEM0NS4wNzk5NjE3LDY2LjAxNjMyOTkgNTIuMDc0OTUyNSw2Mi45OTkxNjc2IDUyLjA4NTk1MjUsNjAuMjkwNDY2IEw1Mi4wODU5NTI1LDU0Ljg1MDI1MDYgWiBNNTIuMDg2OTUyNSw0NC41MjIyNzIgTDU0LjA4Njk0OTksNDQuNTExMzYxOCBMNTQuMDg2OTQ5OSw0NC41MjIyNzIgQzU0LjA4Njk0OTksNDUuNzMwMzI3MSA1My40ODE5NTA3LDQ2Ljg1ODA0MzYgNTIuMzAzOTUyMiw0Ny44OTA1NDM5IEM1My43MzE5NTAzLDQ5LjE0NzE5OSA1NC4wODY5NDk5LDUwLjM4MDA0OTkgNTQuMDg2OTQ5OSw1MS4yNTc4MjQgQzU0LjA4Njk0OTksNTEuMjYzNzc1IDU0LjA4NTk0OTksNTEuMjY4NzM0MiA1NC4wODU5NDk5LDUxLjI3NDY4NTIgTDU0LjA4NTk0OTksNjAuMjg0NTE1IEw1NC4wODY5NDk5LDYwLjI4NDUxNSBDNTQuMDg2OTQ5OSw2NS4yOTUyNjU4IDQ0LjI3NDk2MjgsNjggMzUuMDQzOTc0OSw2OCBDMjUuODM0OTg3MSw2OCAxNi4wNDk5OTk5LDY1LjMwNzE2NzggMTYuMDAzLDYwLjMxOTIyOTIgQzE2LjAwMyw2MC4zMTQyNyAxNiw2MC4zMDkzMTA5IDE2LDYwLjMwNDM1MTcgTDE2LDUxLjI1NDg0ODUgQzE2LDUxLjI1Mjg2NDggMTYuMDAyLDUxLjI0OTg4OTMgMTYuMDAyLDUxLjI0NjkxMzggQzE2LjAwNSw1MC4zNjkxMzk4IDE2LjM2MDk5OTUsNDkuMTQxMjQ3OSAxNy43ODY5OTc2LDQ3Ljg4NzU2ODQgQzE2LjM2OTk5OTUsNDYuNjM1ODcyNSAxNi4wMSw0NS40MTQ5MjM2IDE2LjAwMSw0NC41NDQwOTI0IEwxNi4wMDIsNDQuNTQ0MDkyNCBDMTYuMDAyLDQ0LjU0MDEyNSAxNiw0NC41MzcxNDk1IDE2LDQ0LjUzMzE4MjIgTDE2LDM1LjQ4MzY3OSBDMTYsMzUuNDgwNzAzNSAxNi4wMDIsMzUuNDc3NzI4IDE2LjAwMiwzNS40NzQ3NTI1IEMxNi4wMDUsMzQuNTk2OTc4NCAxNi4zNjE5OTk1LDMzLjM2OTA4NjYgMTcuNzg3OTk3NiwzMi4xMTczOTA4IEMxNi4zNjk5OTk1LDMwLjg2NDcwMzEgMTYuMDEsMjkuNjQyNzYyMyAxNi4wMDEsMjguNzcyOTIyOSBMMTYuMDAyLDI4Ljc3MjkyMjkgQzE2LjAwMiwyOC43Njg5NTU2IDE2LDI4Ljc2NDk4ODIgMTYsMjguNzYxMDIwOSBMMTYsMTkuNzEyNTA5NSBDMTYsMTkuNzA5NTM0IDE2LjAwMiwxOS43MDY1NTg1IDE2LjAwMiwxOS43MDM1ODMgQzE2LjAxOSwxNC42OTk3NzUxIDI1LjgxOTk4NzEsMTIgMzUuMDQzOTc0OSwxMiBDNDAuMjU0OTY4MSwxMiA0NS4yNjA5NjE1LDEyLjgyODE4MjMgNDguNzc3OTU2OSwxNC4yNzIyOTQxIEw0OC4wMTI5NTc5LDE2LjEwNTIwNTQgQzQ0LjcyOTk2MjIsMTQuNzU3MzAxNSA0MC4wMDI5Njg0LDEzLjk4MzY3MDEgMzUuMDQzOTc0OSwxMy45ODM2NzAxIEMyNC45OTk5ODgyLDEzLjk4MzY3MDEgMTguMDAwOTk3NCwxNy4wMDQ3OTk4IDE4LjAwMDk5NzQsMTkuNzE3NDY4NyBDMTguMDAwOTk3NCwyMi40MjkxNDU4IDI0Ljk5OTk4ODIsMjUuNDUwMjc1NCAzNS4wNDM5NzQ5LDI1LjQ1MDI3NTQgQzM1LjMxNDk3NDYsMjUuNDUzMjUwOSAzNS41Nzk5NzQyLDI1LjQ1MDI3NTQgMzUuODQ3OTczOSwyNS40NDAzNTcxIEwzNS45MzE5NzM4LDI3LjQyMjA0MzUgQzM1LjYzNTk3NDIsMjcuNDMzOTQ1NiAzNS4zMzk5NzQ1LDI3LjQzMzk0NTYgMzUuMDQzOTc0OSwyNy40MzM5NDU2IEMyOC4zNDE5ODM4LDI3LjQzMzk0NTYgMjEuMzM2OTkzLDI2LjAwNjY5NDkgMTgsMjMuMzA3OTExNyBMMTgsMjguNzQwMTkyMyBMMTguMDAwOTk3NCwyOC43NDAxOTIzIEwxOC4wMDA5OTc0LDI4Ljc2MzAwNDYgQzE4LjAxMDk5NzQsMjkuODAzNDM5NSAxOS4wNzc5OTU5LDMwLjcxMTk2MDUgMTkuOTcxOTk0OCwzMS4yODkyMDg1IEMyMi42NjE5OTEyLDMzLjAwNDA5MTMgMjcuNDgxOTg0OSwzNC4xNzU0NDg1IDMyLjg1Njk3NzgsMzQuNDE4NDQ4MSBMMzIuNzY1OTc3OSwzNi40MDAxMzQ2IEMyNy4zMjA5ODUxLDM2LjE1MzE2NzcgMjIuNTUyOTkxNCwzNS4wMjM0Njc1IDE5LjQ4Mzk5NTQsMzMuMjkxNzIzNSBDMTguNzI3OTk2NCwzMy44NTcwNjk1IDE4LjAwMDk5NzQsMzQuNjIxNzc0MyAxOC4wMDA5OTc0LDM1LjQ4ODYzODIgQzE4LjAwMDk5NzQsMzguMjAwMzE1MyAyNC45OTk5ODgyLDQxLjIyMTQ0NDkgMzUuMDQzOTc0OSw0MS4yMjE0NDQ5IEMzNi4wMjg5NzM2LDQxLjIyMTQ0NDkgMzcuMDA2OTcyMyw0MS4xODg3MTQzIDM3Ljk1MTk3MTEsNDEuMTIzMjUzMiBMMzguMDkwOTcwOSw0My4xMDE5NjQyIEMzNy4xMDA5NzIyLDQzLjE3MDQwMDggMzYuMDc0OTczNiw0My4yMDUxMTUgMzUuMDQzOTc0OSw0My4yMDUxMTUgQzI4LjM0MTk4MzgsNDMuMjA1MTE1IDIxLjMzNjk5Myw0MS43Nzc4NjQ0IDE4LDM5LjA3OTA4MTEgTDE4LDQ0LjUxMTM2MTggTDE4LjAwMDk5NzQsNDQuNTExMzYxOCBDMTguMDEwOTk3NCw0NS41NzQ2MDkgMTkuMDc3OTk1OSw0Ni40ODIxMzgxIDE5Ljk3MTk5NDgsNDcuMDYwMzc4IEMyMy4wNDc5OTA3LDQ5LjAyMzIxOTYgMjguODIzOTgzMSw1MC4yNDUxNjA0IDM1LjA0Mzk3NDksNTAuMjQ1MTYwNCBMMzUuNDgzOTc0NCw1MC4yNDUxNjA0IEwzNS40ODM5NzQ0LDUyLjIyODgzMDUgTDM1LjA0Mzk3NDksNTIuMjI4ODMwNSBDMjguNzI0OTgzMiw1Mi4yMjg4MzA1IDIyLjk4MTk5MDgsNTEuMDU1NDg5NiAxOS40Njk5OTU0LDQ5LjA3MjgxMTMgQzE4LjcxNzk5NjQsNDkuNjM3MTY1NSAxOC4wMDA5OTc0LDUwLjM5NzkwMyAxOC4wMDA5OTc0LDUxLjI1NzgyNCBDMTguMDAwOTk3NCw1My45Njk1MDExIDI0Ljk5OTk4ODIsNTYuOTkxNjIyNSAzNS4wNDM5NzQ5LDU2Ljk5MTYyMjUgQzQ1LjA3OTk2MTcsNTYuOTkxNjIyNSA1Mi4wNzQ5NTI1LDUzLjk3NDQ2MDIgNTIuMDg1OTUyNSw1MS4yNjQ3NjY4IEw1Mi4wODU5NTI1LDUxLjI1NDg0ODUgTDUyLjA4NTk1MjUsNTEuMjUzODU2NiBDNTIuMDgzOTUyNSw1MC4zOTE5NTIgNTEuMzYzOTUzNCw0OS42MzEyMTQ1IDUwLjYwOTk1NDQsNDkuMDY2ODYwMyBDNTAuMTIxOTU1MSw0OS4zNDM1ODIzIDQ5LjU5ODk1NTgsNDkuNjEwMzg1OSA0OS4wMDM5NTY2LDQ5Ljg1NTM2OTIgTDQ4LjIzNzk1NzYsNDguMDIyNDU4IEM0OC45NjM5NTY2LDQ3LjcyMzkxNTYgNDkuNTkzOTU1OCw0Ny40MDE1NjkyIDUwLjExMDk1NTEsNDcuMDYyMzYxNiBDNTEuMDEyOTUzOSw0Ni40NzQyMDM0IDUyLjA4Njk1MjUsNDUuNTU0NzcyMyA1Mi4wODY5NTI1LDQ0LjUyMjI3MiBMNTIuMDg2OTUyNSw0NC41MjIyNzIgWiBNNjAuNjUyOTQxMiwzMC4wMTY2ODQxIEw1NS4wNDg5NDg2LDMwLjAxNjY4NDEgQzU0LjcxNzk0OSwzMC4wMTY2ODQxIDU0LjQwNjk0OTQsMjkuODU0MDIzMSA1NC4yMjE5NDk3LDI5LjU4MjI2MDMgQzU0LjAzNDk0OTksMjkuMzEwNDk3NSA1My45OTY5NSwyOC45NjQzNDcxIDU0LjExODk0OTgsMjguNjU5ODUzNyBMNTcuNTI3OTQ1MywyMC4xMzgwMDY4IEw0NC42MTg5NzAyLDIwLjEzODAwNjggTDM4LjYxODk3MDIsMzIuMDQwMDI3NiBMNDUuMDAwOTYxOCwzMi4wNDAwMjc2IEM0NS4zMTk5NjE0LDMyLjA0MDAyNzYgNDUuNjE5OTYxLDMyLjE5MTc3ODQgNDUuODA4OTYwOCwzMi40NDY2OCBDNDUuOTk1OTYwNSwzMi43MDI1NzM1IDQ2LjA1MDk2MDQsMzMuMDMwODcwOSA0NS45NTM5NjA2LDMzLjMzMzM4MDYgTDQwLjI1Nzk2ODEsNTEuMDg5MjEyIEw2MC42NTI5NDEyLDMwLjAxNjY4NDEgWiBNNjMuNzIxOTM3MiwyOS43MTIxOTA3IEwzOC43MjI5NzAxLDU1LjUzOTU3NiBDMzguNTI3OTcwMyw1NS43Mzk5MjY3IDM4LjI2NTk3MDcsNTUuODQ0MDY5NCAzOC4wMDA5NzEsNTUuODQ0MDY5NCBDMzcuODI0OTcxMyw1NS44NDQwNjk0IDM3LjY0Nzk3MTUsNTUuNzk5NDM2OCAzNy40ODk5NzE3LDU1LjcwNTIxMjQgQzM3LjA4OTk3MjIsNTUuNDY5MTU1NyAzNi45MDY5NzI1LDU0Ljk5MjA4MyAzNy4wNDc5NzIzLDU0LjU1MTcwODMgTDQzLjYzMzk2MzYsMzQuMDIzNjk3OCBMMzcuMDAwOTcyNCwzNC4wMjM2OTc4IEMzNi42NTM5NzI4LDM0LjAyMzY5NzggMzYuMzMyOTczMiwzMy44NDYxNTkzIDM2LjE0OTk3MzUsMzMuNTUzNTY3OSBDMzUuOTY3OTczNywzMy4yNjA5NzY2IDM1Ljk1MDk3MzcsMzIuODk1OTgxMyAzNi4xMDY5NzM1LDMyLjU4ODUxMjQgTDQzLjEwNjk2NDMsMTguNzAyODIxNCBDNDMuMjc1OTY0MSwxOC4zNjY1ODkzIDQzLjYyMTk2MzYsMTguMTU0MzM2NiA0NC4wMDA5NjMxLDE4LjE1NDMzNjYgTDU5LjAwMDk0MzQsMTguMTU0MzM2NiBDNTkuMzMxOTQzLDE4LjE1NDMzNjYgNTkuNjQyOTQyNSwxOC4zMTc5ODk0IDU5LjgyNzk0MjMsMTguNTg4NzYwNCBDNjAuMDE0OTQyMSwxOC44NjE1MTUgNjAuMDUyOTQyLDE5LjIwNjY3MzYgNTkuOTMwOTQyMiwxOS41MTIxNTg4IEw1Ni41MjE5NDY3LDI4LjAzMzAxMzkgTDYyLjk5OTkzODEsMjguMDMzMDEzOSBDNjMuMzk5OTM3NiwyOC4wMzMwMTM5IDYzLjc2MjkzNzEsMjguMjcxMDU0NCA2My45MTk5MzY5LDI4LjYzNjA0OTcgQzY0LjA3NjkzNjcsMjkuMDAyMDM2OCA2My45OTg5MzY4LDI5LjQyNTU1MDQgNjMuNzIxOTM3MiwyOS43MTIxOTA3IEw2My43MjE5MzcyLDI5LjcxMjE5MDcgWiBNMTkuNDU0OTk1NSw2MC42NzQzMDYyIEMyMC44NzE5OTM2LDYxLjQ3MjczMzQgMjIuNjU1OTkxMiw2Mi4xNDQyMDU3IDI0Ljc1Njk4ODUsNjIuNjY3ODk0NyBMMjUuMjQ0OTg3OCw2MC43NDM3MzQ2IEMyMy4zNDU5OTAzLDYwLjI3MDYyOTMgMjEuNjg1OTkyNSw1OS42NDk3NDA1IDIwLjQ0Mjk5NDIsNTguOTQ5NTA1IEwxOS40NTQ5OTU1LDYwLjY3NDMwNjIgWiBNMjQuNzU2OTg4NSw0Ni43OTg1MzM1IEwyNS4yNDQ5ODc4LDQ0Ljg3NTM2NTMgQzIzLjM0NTk5MDMsNDQuNDAxMjY4MSAyMS42ODU5OTI1LDQzLjc4MDM3OTQgMjAuNDQyOTk0Miw0My4wODAxNDM4IEwxOS40NTQ5OTU1LDQ0LjgwNDk0NSBDMjAuODcxOTkzNiw0NS42MDMzNzIyIDIyLjY1NDk5MTIsNDYuMjc0ODQ0NiAyNC43NTY5ODg1LDQ2Ljc5ODUzMzUgTDI0Ljc1Njk4ODUsNDYuNzk4NTMzNSBaIE0xOS40NTQ5OTU1LDI4LjkzNTU4MzkgTDIwLjQ0Mjk5NDIsMjcuMjEwNzgyNyBDMjEuNjgzOTkyNSwyNy45MTEwMTgyIDIzLjM0NDk5MDMsMjguNTMwOTE1MSAyNS4yNDQ5ODc4LDI5LjAwNjAwNDEgTDI0Ljc1Njk4ODUsMzAuOTI5MTcyMyBDMjIuNjUyOTkxMiwzMC40MDQ0OTE2IDIwLjg2OTk5MzYsMjkuNzMzMDE5MyAxOS40NTQ5OTU1LDI4LjkzNTU4MzkgTDE5LjQ1NDk5NTUsMjguOTM1NTgzOSBaIiBpZD0iQW1hem9uLUR5bmFtb0RCX0ljb25fNjRfU3F1aWQiIGZpbGw9IiNGRkZGRkYiPjwvcGF0aD4KICAgIDwvZz4KPC9zdmc+", + "layout": [ + { + "h": 6, + "i": "9e1d91ec-fb66-4cff-b5c5-282270ebffb5", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 0 + }, + { + "h": 6, + "i": "9a2daf2e-39bc-445d-947f-617c27fadd0f", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 0 + }, + { + "h": 6, + "i": "5b50997d-3bca-466a-bdeb-841b2e49fd65", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 6 + }, + { + "h": 6, + "i": "889c36ab-4d0c-4328-9c3c-6558aad6be89", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 6 + }, + { + "h": 6, + "i": "0c3b97fe-56e0-4ce6-99f4-fd1cbd24f93e", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 12 + }, + { + "h": 6, + "i": "70980d38-ee3c-47be-9520-e371df3b021a", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 12 + }, + { + "h": 6, + "i": "fe1b71b5-1a3f-41c0-b6c2-46bf934787ad", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 18 + }, + { + "h": 6, + "i": "cc0938a5-af82-4bd8-b10e-67eabe717ee0", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 18 + }, + { + "h": 6, + "i": "4bb63c27-5eb4-4904-9947-42ffce15e92e", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 24 + }, + { + "h": 6, + "i": "5ffbe527-8cf3-4ed8-ac2d-8739fa7fa9af", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 24 + }, + { + "h": 6, + "i": "a02f64ac-e73e-4d4c-a26b-fcfc4265c148", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 30 + }, + { + "h": 6, + "i": "014e377d-b7c1-4469-a137-be34d7748f31", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 30 + }, + { + "h": 6, + "i": "b1b75926-7308-43b3-bcad-60f369715f0b", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 36 + }, + { + "h": 6, + "i": "90f4d19d-8785-4a7a-97cf-c967108e1487", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 36 + }, + { + "h": 6, + "i": "5412cdad-174b-462b-916e-4e3de477446b", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 42 + } + ], + "panelMap": {}, + "tags": [], + "title": "DynamoDB Overview", + "uploadedGrafana": false, + "variables": { + "1f7a94df-9735-4bfa-a1b8-dca8ac29f945": { + "allSelected": false, + "customValue": "", + "description": "Account Region", + "id": "1f7a94df-9735-4bfa-a1b8-dca8ac29f945", + "key": "1f7a94df-9735-4bfa-a1b8-dca8ac29f945", + "modificationUUID": "8ef772a1-7df9-46a2-84e7-ab0c0bfc6886", + "multiSelect": false, + "name": "Region", + "order": 1, + "queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.region') AS region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name like '%aws_DynamoDB%' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} GROUP BY region", + "showALLOption": false, + "sort": "DISABLED", + "textboxValue": "", + "type": "QUERY" + }, + "93ee15bf-baab-4abf-8828-fe6e75518417": { + "allSelected": false, + "customValue": "", + "description": "AWS Account ID", + "id": "93ee15bf-baab-4abf-8828-fe6e75518417", + "key": "93ee15bf-baab-4abf-8828-fe6e75518417", + "modificationUUID": "409e6a7e-1ec1-4611-8624-492a3aac6ca0", + "multiSelect": false, + "name": "Account", + "order": 0, + "queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud.account.id') AS `cloud.account.id`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name like '%aws_DynamoDB%' GROUP BY `cloud.account.id`", + "showALLOption": false, + "sort": "ASC", + "textboxValue": "", + "type": "QUERY" + }, + "fd28f0e0-d4ec-4bcd-9c45-32395cb0c55b": { + "allSelected": true, + "customValue": "", + "description": "DynamoDB Tables", + "id": "fd28f0e0-d4ec-4bcd-9c45-32395cb0c55b", + "modificationUUID": "8ebb9032-7e56-4981-8036-efdfc413f8a8", + "multiSelect": true, + "name": "Table", + "order": 2, + "queryValue": "SELECT DISTINCT JSONExtractString(labels, 'TableName') AS table FROM signoz_metrics.distributed_time_series_v4_1day WHERE metric_name like '%aws_DynamoDB%' AND JSONExtractString(labels, 'cloud.account.id') IN {{.Account}} AND JSONExtractString(labels, 'cloud.region') IN {{.Region}} and table != '' GROUP BY table\n", + "showALLOption": true, + "sort": "ASC", + "textboxValue": "", + "type": "QUERY" + } + }, + "version": "v4", + "widgets": [ + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "9e1d91ec-fb66-4cff-b5c5-282270ebffb5", + "isLogScale": false, + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_DynamoDB_AccountMaxReads_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_DynamoDB_AccountMaxReads_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "fc55895c", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "8b3f3e0b", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [], + "having": [], + "legend": "", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "4fdb1c6c-8c7f-4f8b-a468-9326c811981a", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Account Max Reads", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "5b50997d-3bca-466a-bdeb-841b2e49fd65", + "isLogScale": false, + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_DynamoDB_AccountMaxTableLevelReads_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_DynamoDB_AccountMaxTableLevelReads_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "f7b176f8", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "9a023ab7", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [], + "having": [], + "legend": "", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "310efa3b-d68a-4630-b279-bcbc22ddbefb", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Account Max Table Level Reads", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "889c36ab-4d0c-4328-9c3c-6558aad6be89", + "isLogScale": false, + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_DynamoDB_AccountMaxTableLevelWrites_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_DynamoDB_AccountMaxTableLevelWrites_max", + "type": "Gauge" + }, + "aggregateOperator": "avg", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "ec5ebf95", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "5b2fb00e", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [], + "having": [], + "legend": "", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "avg" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "473de955-bc5c-4a66-aa8d-2e37502c5643", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Account Max Table Level Writes", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "9a2daf2e-39bc-445d-947f-617c27fadd0f", + "isLogScale": false, + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_DynamoDB_AccountMaxWrites_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_DynamoDB_AccountMaxWrites_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "3815cf09", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "a783bd91", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [], + "having": [], + "legend": "", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "avg", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "1115aaa1-fdb0-47a1-af79-8c6d439747d4", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Account Max Writes", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "0c3b97fe-56e0-4ce6-99f4-fd1cbd24f93e", + "isLogScale": false, + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_DynamoDB_AccountProvisionedReadCapacityUtilization_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_DynamoDB_AccountProvisionedReadCapacityUtilization_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "edcbcb83", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "224766cb", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [], + "having": [], + "legend": "", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "d42bc3cd-f457-42eb-936e-c931b0c77f61", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Account Provisioned Read Capacity", + "yAxisUnit": "percent" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "70980d38-ee3c-47be-9520-e371df3b021a", + "isLogScale": false, + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_DynamoDB_AccountProvisionedWriteCapacityUtilization_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_DynamoDB_AccountProvisionedWriteCapacityUtilization_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "c237482a", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "e3a117d5", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [], + "having": [], + "legend": "", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "d06d2f3d-8878-4c53-a8f1-10024091887a", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Account Provisioned Write Capacity", + "yAxisUnit": "percent" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "fe1b71b5-1a3f-41c0-b6c2-46bf934787ad", + "isLogScale": false, + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_DynamoDB_ConsumedReadCapacityUnits_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_DynamoDB_ConsumedReadCapacityUnits_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "b867513b", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "9c10cbaa", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "4ff7fb7c", + "key": { + "dataType": "string", + "id": "TableName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "TableName", + "type": "tag" + }, + "op": "in", + "value": [ + "$Table" + ] + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "TableName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "TableName", + "type": "tag" + } + ], + "having": [], + "legend": "{{TableName}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "32c9f178-073c-4d1f-8193-76f804776df0", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Consumed Read Capacity", + "yAxisUnit": "percent" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "cc0938a5-af82-4bd8-b10e-67eabe717ee0", + "isLogScale": false, + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_DynamoDB_ConsumedWriteCapacityUnits_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_DynamoDB_ConsumedWriteCapacityUnits_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "7e2aa806", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "dd49e062", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "e7ada865", + "key": { + "dataType": "string", + "id": "TableName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "TableName", + "type": "tag" + }, + "op": "in", + "value": [ + "$Table" + ] + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "TableName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "TableName", + "type": "tag" + } + ], + "having": [], + "legend": "{{TableName}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "40397368-92df-42b9-b0e6-0e7dc7984bc4", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Consumed Write Capacity", + "yAxisUnit": "percent" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "4bb63c27-5eb4-4904-9947-42ffce15e92e", + "isLogScale": false, + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_DynamoDB_MaxProvisionedTableReadCapacityUtilization_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_DynamoDB_MaxProvisionedTableReadCapacityUtilization_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "b3e029fa", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "e6764d50", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [], + "having": [], + "legend": "", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "6a33d44a-a337-422f-a964-89b88804343f", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Max Provisioned Table Read Capacity", + "yAxisUnit": "percent" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "5ffbe527-8cf3-4ed8-ac2d-8739fa7fa9af", + "isLogScale": false, + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_DynamoDB_MaxProvisionedTableWriteCapacityUtilization_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_DynamoDB_MaxProvisionedTableWriteCapacityUtilization_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "80ba9142", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "9c802cf0", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [], + "having": [], + "legend": "", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "a98b7d13-63d3-46cf-b4e7-686b3be7d9f9", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Max Provisioned Table Write Capacity", + "yAxisUnit": "percent" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "a02f64ac-e73e-4d4c-a26b-fcfc4265c148", + "isLogScale": false, + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_DynamoDB_ReturnedItemCount_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_DynamoDB_ReturnedItemCount_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "db6edb77", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "8b86de4a", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "a8d39d03", + "key": { + "dataType": "string", + "id": "TableName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "TableName", + "type": "tag" + }, + "op": "in", + "value": [ + "$Table" + ] + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "TableName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "TableName", + "type": "tag" + } + ], + "having": [], + "legend": "{{TableName}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "6322f225-471d-43a2-b13e-f2312c1a7b57", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Returned Item Count", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "014e377d-b7c1-4469-a137-be34d7748f31", + "isLogScale": false, + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_DynamoDB_SuccessfulRequestLatency_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_DynamoDB_SuccessfulRequestLatency_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "93bef7f0", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "4a293ec8", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "2e2286c6", + "key": { + "dataType": "string", + "id": "TableName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "TableName", + "type": "tag" + }, + "op": "in", + "value": [ + "$Table" + ] + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "TableName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "TableName", + "type": "tag" + } + ], + "having": [], + "legend": "{{TableName}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "6ad1cbfe-9581-4d99-a14e-50bc5fef699f", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Successful Request Latency", + "yAxisUnit": "ms" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "b1b75926-7308-43b3-bcad-60f369715f0b", + "isLogScale": false, + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_DynamoDB_ThrottledRequests_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_DynamoDB_ThrottledRequests_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "28fcd3cd", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "619578e5", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "a6bc481e", + "key": { + "dataType": "string", + "id": "TableName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "TableName", + "type": "tag" + }, + "op": "in", + "value": [ + "$Table" + ] + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "TableName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "TableName", + "type": "tag" + } + ], + "having": [], + "legend": "{{TableName}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "fd358cf0-a0b0-4106-a89c-a5196297c23b", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Max Throttled Requests", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "5412cdad-174b-462b-916e-4e3de477446b", + "isLogScale": false, + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_DynamoDB_UserErrors_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_DynamoDB_UserErrors_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "5a060b5e", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "3a1cb5ff", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [], + "having": [], + "legend": "", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "17db2e6d-d9dc-4568-85ea-ea4b373dfc5e", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "User Errors", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "90f4d19d-8785-4a7a-97cf-c967108e1487", + "isLogScale": false, + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_DynamoDB_WriteThrottleEvents_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_DynamoDB_WriteThrottleEvents_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "58bc06b3", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "d6d7a8fb", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [], + "having": [], + "legend": "", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "713c6c70-3a62-4b67-8a67-7917ca9d4fbf", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Max Write Throttle Events", + "yAxisUnit": "none" + } + ] +} diff --git a/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/dynamodb/icon.svg b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/dynamodb/icon.svg new file mode 100644 index 00000000000..bd4f2c30f50 --- /dev/null +++ b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/dynamodb/icon.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_Amazon-DynamoDB_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/dynamodb/integration.json b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/dynamodb/integration.json new file mode 100644 index 00000000000..8590108cff5 --- /dev/null +++ b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/dynamodb/integration.json @@ -0,0 +1,395 @@ +{ + "id": "dynamodb", + "title": "DynamoDB", + "icon": "file://icon.svg", + "overview": "file://overview.md", + "supportedSignals": { + "metrics": true, + "logs": false + }, + "dataCollected": { + "metrics": [ + { + "name": "aws_DynamoDB_AccountMaxReads_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_AccountMaxReads_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_AccountMaxReads_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_AccountMaxReads_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_AccountMaxTableLevelReads_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_AccountMaxTableLevelReads_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_AccountMaxTableLevelReads_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_AccountMaxTableLevelReads_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_AccountMaxTableLevelWrites_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_AccountMaxTableLevelWrites_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_AccountMaxTableLevelWrites_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_AccountMaxTableLevelWrites_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_AccountMaxWrites_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_AccountMaxWrites_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_AccountMaxWrites_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_AccountMaxWrites_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_AccountProvisionedReadCapacityUtilization_count", + "unit": "Percent", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_AccountProvisionedReadCapacityUtilization_max", + "unit": "Percent", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_AccountProvisionedReadCapacityUtilization_min", + "unit": "Percent", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_AccountProvisionedReadCapacityUtilization_sum", + "unit": "Percent", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_AccountProvisionedWriteCapacityUtilization_count", + "unit": "Percent", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_AccountProvisionedWriteCapacityUtilization_max", + "unit": "Percent", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_AccountProvisionedWriteCapacityUtilization_min", + "unit": "Percent", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_AccountProvisionedWriteCapacityUtilization_sum", + "unit": "Percent", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_ConsumedReadCapacityUnits_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_ConsumedReadCapacityUnits_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_ConsumedReadCapacityUnits_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_ConsumedReadCapacityUnits_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_ConsumedWriteCapacityUnits_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_ConsumedWriteCapacityUnits_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_ConsumedWriteCapacityUnits_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_ConsumedWriteCapacityUnits_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_MaxProvisionedTableReadCapacityUtilization_count", + "unit": "Percent", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_MaxProvisionedTableReadCapacityUtilization_max", + "unit": "Percent", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_MaxProvisionedTableReadCapacityUtilization_min", + "unit": "Percent", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_MaxProvisionedTableReadCapacityUtilization_sum", + "unit": "Percent", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_MaxProvisionedTableWriteCapacityUtilization_count", + "unit": "Percent", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_MaxProvisionedTableWriteCapacityUtilization_max", + "unit": "Percent", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_MaxProvisionedTableWriteCapacityUtilization_min", + "unit": "Percent", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_MaxProvisionedTableWriteCapacityUtilization_sum", + "unit": "Percent", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_ReturnedItemCount_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_ReturnedItemCount_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_ReturnedItemCount_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_ReturnedItemCount_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_SuccessfulRequestLatency_count", + "unit": "Milliseconds", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_SuccessfulRequestLatency_max", + "unit": "Milliseconds", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_SuccessfulRequestLatency_min", + "unit": "Milliseconds", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_SuccessfulRequestLatency_sum", + "unit": "Milliseconds", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_ThrottledRequests_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_ThrottledRequests_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_ThrottledRequests_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_ThrottledRequests_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_UserErrors_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_UserErrors_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_UserErrors_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_UserErrors_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_WriteThrottleEvents_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_WriteThrottleEvents_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_WriteThrottleEvents_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_DynamoDB_WriteThrottleEvents_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + } + ] + }, + "telemetryCollectionStrategy": { + "aws": { + "metrics": { + "cloudwatchMetricStreamFilters": [ + { + "Namespace": "AWS/DynamoDB" + } + ] + } + } + }, + "assets": { + "dashboards": [ + { + "id": "overview", + "title": "DynamoDB Overview", + "description": "Overview of DynamoDB", + "definition": "file://assets/dashboards/overview.json" + } + ] + } +} diff --git a/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/dynamodb/overview.md b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/dynamodb/overview.md new file mode 100644 index 00000000000..3de918d29a4 --- /dev/null +++ b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/dynamodb/overview.md @@ -0,0 +1,3 @@ +### Monitor DynamoDB with SigNoz + +Collect DynamoDB Key Metrics and view them with an out of the box dashboard. diff --git a/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ec2/assets/dashboards/overview.json b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ec2/assets/dashboards/overview.json new file mode 100644 index 00000000000..3dba9c50bbd --- /dev/null +++ b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ec2/assets/dashboards/overview.json @@ -0,0 +1,1446 @@ +{ + "description": "Overview of EC2 instances", + "image": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAxNiAxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBmaWxsPSJub25lIj4KICA8cGF0aCBmaWxsPSIjOUQ1MDI1IiBkPSJNMS43MDIgMi45OEwxIDMuMzEydjkuMzc2bC43MDIuMzMyIDIuODQyLTQuNzc3TDEuNzAyIDIuOTh6IiAvPgogIDxwYXRoIGZpbGw9IiNGNTg1MzYiIGQ9Ik0zLjMzOSAxMi42NTdsLTEuNjM3LjM2M1YyLjk4bDEuNjM3LjM1M3Y5LjMyNHoiIC8+CiAgPHBhdGggZmlsbD0iIzlENTAyNSIgZD0iTTIuNDc2IDIuNjEybC44NjMtLjQwNiA0LjA5NiA2LjIxNi00LjA5NiA1LjM3Mi0uODYzLS40MDZWMi42MTJ6IiAvPgogIDxwYXRoIGZpbGw9IiNGNTg1MzYiIGQ9Ik01LjM4IDEzLjI0OGwtMi4wNDEuNTQ2VjIuMjA2bDIuMDQuNTQ4djEwLjQ5NHoiIC8+CiAgPHBhdGggZmlsbD0iIzlENTAyNSIgZD0iTTQuMyAxLjc1bDEuMDgtLjUxMiA2LjA0MyA3Ljg2NC02LjA0MyA1LjY2LTEuMDgtLjUxMVYxLjc0OXoiIC8+CiAgPHBhdGggZmlsbD0iI0Y1ODUzNiIgZD0iTTcuOTk4IDEzLjg1NmwtMi42MTguOTA2VjEuMjM4bDIuNjE4LjkwOHYxMS43MXoiIC8+CiAgPHBhdGggZmlsbD0iIzlENTAyNSIgZD0iTTYuNjAyLjY2TDcuOTk4IDBsNi41MzggOC40NTNMNy45OTggMTZsLTEuMzk2LS42NlYuNjZ6IiAvPgogIDxwYXRoIGZpbGw9IiNGNTg1MzYiIGQ9Ik0xNSAxMi42ODZMNy45OTggMTZWMEwxNSAzLjMxNHY5LjM3MnoiIC8+Cjwvc3ZnPg==", + "layout": [ + { + "h": 5, + "i": "b8a20569-7e4f-40d0-ada6-7736cfadae06", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 0 + }, + { + "h": 5, + "i": "b668ba49-d126-4f2d-9eb5-b4cfbfaf94d1", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 0 + }, + { + "h": 5, + "i": "6fced7be-8a73-4b9b-8440-f2142230268c", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 5 + }, + { + "h": 5, + "i": "20dbaec7-9a16-47ad-af3b-f56375db8e69", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 5 + }, + { + "h": 5, + "i": "827a354b-1fff-400b-8172-c41e4c830eb5", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 10 + }, + { + "h": 5, + "i": "05de543a-73a2-4221-b784-263749d39b1e", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 10 + } + ], + "panelMap": {}, + "tags": [], + "title": "EC2 Overview", + "uploadedGrafana": false, + "variables": { + "11b96105-b752-47ef-88bc-832f248cf855": { + "allSelected": false, + "customValue": "", + "description": "AWS Account", + "id": "11b96105-b752-47ef-88bc-832f248cf855", + "key": "11b96105-b752-47ef-88bc-832f248cf855", + "modificationUUID": "23866855-0966-45ae-99cd-53fab002a1fa", + "multiSelect": false, + "name": "Account", + "order": 0, + "queryValue": "SELECT JSONExtractString(labels, 'cloud_account_id') as cloud_account_id\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like 'aws_EC2_CPUUtilization_sum'\nGROUP BY cloud_account_id", + "showALLOption": false, + "sort": "DISABLED", + "textboxValue": "", + "type": "QUERY" + }, + "63a394bf-4acd-4f14-bf0a-f9dc5e4f00c2": { + "allSelected": false, + "customValue": "", + "description": "AWS Region", + "id": "63a394bf-4acd-4f14-bf0a-f9dc5e4f00c2", + "modificationUUID": "d3060c9c-fa76-4fc3-bcfa-e417c90717fa", + "multiSelect": false, + "name": "Region", + "order": 0, + "queryValue": "\nSELECT JSONExtractString(labels, 'cloud_region') as cloud_region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like 'aws_EC2_CPUUtilization_sum'\n and JSONExtractString(labels, 'cloud_account_id') IN {{.Account}}\nGROUP BY cloud_region", + "showALLOption": false, + "sort": "DISABLED", + "textboxValue": "", + "type": "QUERY" + } + }, + "version": "v4", + "widgets": [ + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "b8a20569-7e4f-40d0-ada6-7736cfadae06", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_EC2_CPUUtilization_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_EC2_CPUUtilization_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "d302d50d", + "key": { + "dataType": "string", + "id": "service_instance_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service_instance_id", + "type": "tag" + }, + "op": "!=", + "value": "" + }, + { + "id": "e6c54e87", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "7907211a", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "service_instance_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service_instance_id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + } + ], + "having": [], + "legend": "{{service_instance_id}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "170f1610-b7d0-4628-aca4-207c122b3709", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "CPU Utilization", + "yAxisUnit": "percent" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "Percentage of available CPU credits that were utilizaed", + "fillSpans": false, + "id": "b668ba49-d126-4f2d-9eb5-b4cfbfaf94d1", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_EC2_CPUCreditUsage_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_EC2_CPUCreditUsage_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "30ded0dc", + "key": { + "dataType": "string", + "id": "service_instance_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service_instance_id", + "type": "tag" + }, + "op": "!=", + "value": "" + }, + { + "id": "c935f6ec", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "d092fef8", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "service_instance_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service_instance_id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + } + ], + "having": [], + "legend": "{{service_instance_id}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "0643cc76-eedd-4101-bd7a-ec810a3e9b8a", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "CPU Credits Utilization", + "yAxisUnit": "percentunit" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "6fced7be-8a73-4b9b-8440-f2142230268c", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_EC2_EBSReadBytes_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_EC2_EBSReadBytes_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "a5fbfa4a", + "key": { + "dataType": "string", + "id": "service_instance_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service_instance_id", + "type": "tag" + }, + "op": "!=", + "value": "" + }, + { + "id": "87071f13", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "c84a88c4", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "service_instance_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service_instance_id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + } + ], + "having": [], + "legend": "{{service_instance_id}} - Reads", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + }, + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_EC2_EBSWriteBytes_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_EC2_EBSWriteBytes_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "B", + "filters": { + "items": [ + { + "id": "4d10ca4b", + "key": { + "dataType": "string", + "id": "service_instance_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service_instance_id", + "type": "tag" + }, + "op": "!=", + "value": "" + }, + { + "id": "fc2db932", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "a3fd74c0", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "service_instance_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service_instance_id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + } + ], + "having": [], + "legend": "{{service_instance_id}} - Writes", + "limit": null, + "orderBy": [], + "queryName": "B", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "43627952-52aa-40fd-9c04-cb0e3c123f98", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "EBS Read/Write Bytes", + "yAxisUnit": "binBps" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "20dbaec7-9a16-47ad-af3b-f56375db8e69", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_EC2_EBSReadOps_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_EC2_EBSReadOps_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "85d84806", + "key": { + "dataType": "string", + "id": "service_instance_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service_instance_id", + "type": "tag" + }, + "op": "!=", + "value": "" + }, + { + "id": "f2074606", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "134c7ca9", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "service_instance_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service_instance_id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + } + ], + "having": [], + "legend": "{{service_instance_id}} - Reads", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + }, + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_EC2_EBSWriteOps_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_EC2_EBSWriteOps_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "B", + "filters": { + "items": [ + { + "id": "47e0c00f", + "key": { + "dataType": "string", + "id": "service_instance_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service_instance_id", + "type": "tag" + }, + "op": "!=", + "value": "" + }, + { + "id": "0a157dfe", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "a7d1e8df", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "service_instance_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service_instance_id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + } + ], + "having": [], + "legend": "{{service_instance_id}} - Writes", + "limit": null, + "orderBy": [], + "queryName": "B", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "a70e41d1-27dc-4e91-bf13-23d8bd2f3c49", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "EBS Read/Write Ops", + "yAxisUnit": "cps" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "827a354b-1fff-400b-8172-c41e4c830eb5", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_EC2_NetworkIn_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_EC2_NetworkIn_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "12d6748d", + "key": { + "dataType": "string", + "id": "service_instance_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service_instance_id", + "type": "tag" + }, + "op": "!=", + "value": "" + }, + { + "id": "df3a8da1", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "81ec53f4", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "service_instance_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service_instance_id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + } + ], + "having": [], + "legend": "{{service_instance_id}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "ac2ce4a6-f595-4d2a-bc22-ddc51c2d59ff", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Network Incoming", + "yAxisUnit": "binBps" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "05de543a-73a2-4221-b784-263749d39b1e", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_EC2_NetworkOut_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_EC2_NetworkOut_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "d301aaa7", + "key": { + "dataType": "string", + "id": "service_instance_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service_instance_id", + "type": "tag" + }, + "op": "!=", + "value": "" + }, + { + "id": "e8afaa3b", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "d67487ab", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "service_instance_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service_instance_id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + } + ], + "having": [], + "legend": "{{service_instance_id}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "94027537-0da8-4ac5-a880-532b975a818c", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Network Outgoing", + "yAxisUnit": "binBps" + } + ] +} \ No newline at end of file diff --git a/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ec2/assets/dashboards/overview.png b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ec2/assets/dashboards/overview.png new file mode 100644 index 0000000000000000000000000000000000000000..fd2740c2942f42dcb0f730d332845f48180e3cf9 GIT binary patch literal 96334 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 literal 0 HcmV?d00001 diff --git a/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ec2/assets/dashboards/overview_dot.json b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ec2/assets/dashboards/overview_dot.json new file mode 100644 index 00000000000..72ed3d5103d --- /dev/null +++ b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ec2/assets/dashboards/overview_dot.json @@ -0,0 +1,1446 @@ +{ + "description": "Overview of EC2 instances", + "image": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAxNiAxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBmaWxsPSJub25lIj4KICA8cGF0aCBmaWxsPSIjOUQ1MDI1IiBkPSJNMS43MDIgMi45OEwxIDMuMzEydjkuMzc2bC43MDIuMzMyIDIuODQyLTQuNzc3TDEuNzAyIDIuOTh6IiAvPgogIDxwYXRoIGZpbGw9IiNGNTg1MzYiIGQ9Ik0zLjMzOSAxMi42NTdsLTEuNjM3LjM2M1YyLjk4bDEuNjM3LjM1M3Y5LjMyNHoiIC8+CiAgPHBhdGggZmlsbD0iIzlENTAyNSIgZD0iTTIuNDc2IDIuNjEybC44NjMtLjQwNiA0LjA5NiA2LjIxNi00LjA5NiA1LjM3Mi0uODYzLS40MDZWMi42MTJ6IiAvPgogIDxwYXRoIGZpbGw9IiNGNTg1MzYiIGQ9Ik01LjM4IDEzLjI0OGwtMi4wNDEuNTQ2VjIuMjA2bDIuMDQuNTQ4djEwLjQ5NHoiIC8+CiAgPHBhdGggZmlsbD0iIzlENTAyNSIgZD0iTTQuMyAxLjc1bDEuMDgtLjUxMiA2LjA0MyA3Ljg2NC02LjA0MyA1LjY2LTEuMDgtLjUxMVYxLjc0OXoiIC8+CiAgPHBhdGggZmlsbD0iI0Y1ODUzNiIgZD0iTTcuOTk4IDEzLjg1NmwtMi42MTguOTA2VjEuMjM4bDIuNjE4LjkwOHYxMS43MXoiIC8+CiAgPHBhdGggZmlsbD0iIzlENTAyNSIgZD0iTTYuNjAyLjY2TDcuOTk4IDBsNi41MzggOC40NTNMNy45OTggMTZsLTEuMzk2LS42NlYuNjZ6IiAvPgogIDxwYXRoIGZpbGw9IiNGNTg1MzYiIGQ9Ik0xNSAxMi42ODZMNy45OTggMTZWMEwxNSAzLjMxNHY5LjM3MnoiIC8+Cjwvc3ZnPg==", + "layout": [ + { + "h": 5, + "i": "b8a20569-7e4f-40d0-ada6-7736cfadae06", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 0 + }, + { + "h": 5, + "i": "b668ba49-d126-4f2d-9eb5-b4cfbfaf94d1", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 0 + }, + { + "h": 5, + "i": "6fced7be-8a73-4b9b-8440-f2142230268c", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 5 + }, + { + "h": 5, + "i": "20dbaec7-9a16-47ad-af3b-f56375db8e69", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 5 + }, + { + "h": 5, + "i": "827a354b-1fff-400b-8172-c41e4c830eb5", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 10 + }, + { + "h": 5, + "i": "05de543a-73a2-4221-b784-263749d39b1e", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 10 + } + ], + "panelMap": {}, + "tags": [], + "title": "EC2 Overview", + "uploadedGrafana": false, + "variables": { + "11b96105-b752-47ef-88bc-832f248cf855": { + "allSelected": false, + "customValue": "", + "description": "AWS Account", + "id": "11b96105-b752-47ef-88bc-832f248cf855", + "key": "11b96105-b752-47ef-88bc-832f248cf855", + "modificationUUID": "23866855-0966-45ae-99cd-53fab002a1fa", + "multiSelect": false, + "name": "Account", + "order": 0, + "queryValue": "SELECT JSONExtractString(labels, 'cloud.account.id') as `cloud.account.id`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like 'aws_EC2_CPUUtilization_sum'\nGROUP BY `cloud.account.id`", + "showALLOption": false, + "sort": "DISABLED", + "textboxValue": "", + "type": "QUERY" + }, + "63a394bf-4acd-4f14-bf0a-f9dc5e4f00c2": { + "allSelected": false, + "customValue": "", + "description": "AWS Region", + "id": "63a394bf-4acd-4f14-bf0a-f9dc5e4f00c2", + "modificationUUID": "d3060c9c-fa76-4fc3-bcfa-e417c90717fa", + "multiSelect": false, + "name": "Region", + "order": 0, + "queryValue": "\nSELECT JSONExtractString(labels, 'cloud.region') as `cloud.region`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE \n metric_name like 'aws_EC2_CPUUtilization_sum'\n and JSONExtractString(labels, 'cloud.account.id') IN {{.Account}}\nGROUP BY `cloud.region`", + "showALLOption": false, + "sort": "DISABLED", + "textboxValue": "", + "type": "QUERY" + } + }, + "version": "v4", + "widgets": [ + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "b8a20569-7e4f-40d0-ada6-7736cfadae06", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_EC2_CPUUtilization_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_EC2_CPUUtilization_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "d302d50d", + "key": { + "dataType": "string", + "id": "service.instance.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service.instance.id", + "type": "tag" + }, + "op": "!=", + "value": "" + }, + { + "id": "e6c54e87", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "7907211a", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "service.instance.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service.instance.id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + } + ], + "having": [], + "legend": "{{service.instance.id}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "170f1610-b7d0-4628-aca4-207c122b3709", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "CPU Utilization", + "yAxisUnit": "percent" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "Percentage of available CPU credits that were utilizaed", + "fillSpans": false, + "id": "b668ba49-d126-4f2d-9eb5-b4cfbfaf94d1", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_EC2_CPUCreditUsage_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_EC2_CPUCreditUsage_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "30ded0dc", + "key": { + "dataType": "string", + "id": "service.instance.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service.instance.id", + "type": "tag" + }, + "op": "!=", + "value": "" + }, + { + "id": "c935f6ec", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "d092fef8", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "service.instance.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service.instance.id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + } + ], + "having": [], + "legend": "{{service.instance.id}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "0643cc76-eedd-4101-bd7a-ec810a3e9b8a", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "CPU Credits Utilization", + "yAxisUnit": "percentunit" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "6fced7be-8a73-4b9b-8440-f2142230268c", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_EC2_EBSReadBytes_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_EC2_EBSReadBytes_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "a5fbfa4a", + "key": { + "dataType": "string", + "id": "service.instance.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service.instance.id", + "type": "tag" + }, + "op": "!=", + "value": "" + }, + { + "id": "87071f13", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "c84a88c4", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "service.instance.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service.instance.id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + } + ], + "having": [], + "legend": "{{service.instance.id}} - Reads", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + }, + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_EC2_EBSWriteBytes_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_EC2_EBSWriteBytes_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "B", + "filters": { + "items": [ + { + "id": "4d10ca4b", + "key": { + "dataType": "string", + "id": "service.instance.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service.instance.id", + "type": "tag" + }, + "op": "!=", + "value": "" + }, + { + "id": "fc2db932", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "a3fd74c0", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "service.instance.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service.instance.id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + } + ], + "having": [], + "legend": "{{service.instance.id}} - Writes", + "limit": null, + "orderBy": [], + "queryName": "B", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "43627952-52aa-40fd-9c04-cb0e3c123f98", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "EBS Read/Write Bytes", + "yAxisUnit": "binBps" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "20dbaec7-9a16-47ad-af3b-f56375db8e69", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_EC2_EBSReadOps_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_EC2_EBSReadOps_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "85d84806", + "key": { + "dataType": "string", + "id": "service.instance.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service.instance.id", + "type": "tag" + }, + "op": "!=", + "value": "" + }, + { + "id": "f2074606", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "134c7ca9", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "service.instance.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service.instance.id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + } + ], + "having": [], + "legend": "{{service.instance.id}} - Reads", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + }, + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_EC2_EBSWriteOps_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_EC2_EBSWriteOps_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "B", + "filters": { + "items": [ + { + "id": "47e0c00f", + "key": { + "dataType": "string", + "id": "service.instance.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service.instance.id", + "type": "tag" + }, + "op": "!=", + "value": "" + }, + { + "id": "0a157dfe", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "a7d1e8df", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "service.instance.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service.instance.id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + } + ], + "having": [], + "legend": "{{service.instance.id}} - Writes", + "limit": null, + "orderBy": [], + "queryName": "B", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "a70e41d1-27dc-4e91-bf13-23d8bd2f3c49", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "EBS Read/Write Ops", + "yAxisUnit": "cps" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "827a354b-1fff-400b-8172-c41e4c830eb5", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_EC2_NetworkIn_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_EC2_NetworkIn_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "12d6748d", + "key": { + "dataType": "string", + "id": "service.instance.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service.instance.id", + "type": "tag" + }, + "op": "!=", + "value": "" + }, + { + "id": "df3a8da1", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "81ec53f4", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "service.instance.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service.instance.id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + } + ], + "having": [], + "legend": "{{service.instance.id}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "ac2ce4a6-f595-4d2a-bc22-ddc51c2d59ff", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Network Incoming", + "yAxisUnit": "binBps" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "description": "", + "fillSpans": false, + "id": "05de543a-73a2-4221-b784-263749d39b1e", + "isStacked": false, + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_EC2_NetworkOut_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_EC2_NetworkOut_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "d301aaa7", + "key": { + "dataType": "string", + "id": "service.instance.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service.instance.id", + "type": "tag" + }, + "op": "!=", + "value": "" + }, + { + "id": "e8afaa3b", + "key": { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "d67487ab", + "key": { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "service.instance.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "service.instance.id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.account.id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.account.id", + "type": "tag" + }, + { + "dataType": "string", + "id": "cloud.region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud.region", + "type": "tag" + } + ], + "having": [], + "legend": "{{service.instance.id}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "94027537-0da8-4ac5-a880-532b975a818c", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Network Outgoing", + "yAxisUnit": "binBps" + } + ] +} diff --git a/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ec2/icon.svg b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ec2/icon.svg new file mode 100644 index 00000000000..6c85e7c8d2c --- /dev/null +++ b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ec2/icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ec2/integration.json b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ec2/integration.json new file mode 100644 index 00000000000..f63bb839198 --- /dev/null +++ b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ec2/integration.json @@ -0,0 +1,519 @@ +{ + "id": "ec2", + "title": "EC2", + "icon": "file://icon.svg", + "overview": "file://overview.md", + "supportedSignals": { + "metrics": true, + "logs": false + }, + "dataCollected": { + "metrics": [ + { + "name": "aws_EC2_CPUCreditBalance_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_CPUCreditBalance_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_CPUCreditBalance_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_CPUCreditBalance_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_CPUCreditUsage_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_CPUCreditUsage_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_CPUCreditUsage_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_CPUCreditUsage_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_CPUSurplusCreditBalance_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_CPUSurplusCreditBalance_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_CPUSurplusCreditBalance_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_CPUSurplusCreditBalance_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_CPUSurplusCreditsCharged_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_CPUSurplusCreditsCharged_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_CPUSurplusCreditsCharged_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_CPUSurplusCreditsCharged_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_CPUUtilization_count", + "unit": "Percent", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_CPUUtilization_max", + "unit": "Percent", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_CPUUtilization_min", + "unit": "Percent", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_CPUUtilization_sum", + "unit": "Percent", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_EBSByteBalance__count", + "unit": "Percent", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_EBSByteBalance__max", + "unit": "Percent", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_EBSByteBalance__min", + "unit": "Percent", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_EBSByteBalance__sum", + "unit": "Percent", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_EBSIOBalance__count", + "unit": "Percent", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_EBSIOBalance__max", + "unit": "Percent", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_EBSIOBalance__min", + "unit": "Percent", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_EBSIOBalance__sum", + "unit": "Percent", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_EBSReadBytes_count", + "unit": "Bytes", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_EBSReadBytes_max", + "unit": "Bytes", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_EBSReadBytes_min", + "unit": "Bytes", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_EBSReadBytes_sum", + "unit": "Bytes", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_EBSReadOps_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_EBSReadOps_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_EBSReadOps_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_EBSReadOps_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_EBSWriteBytes_count", + "unit": "Bytes", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_EBSWriteBytes_max", + "unit": "Bytes", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_EBSWriteBytes_min", + "unit": "Bytes", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_EBSWriteBytes_sum", + "unit": "Bytes", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_EBSWriteOps_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_EBSWriteOps_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_EBSWriteOps_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_EBSWriteOps_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_MetadataNoToken_count", + "unit": "None", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_MetadataNoToken_max", + "unit": "None", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_MetadataNoToken_min", + "unit": "None", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_MetadataNoToken_sum", + "unit": "None", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_NetworkIn_count", + "unit": "Bytes", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_NetworkIn_max", + "unit": "Bytes", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_NetworkIn_min", + "unit": "Bytes", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_NetworkIn_sum", + "unit": "Bytes", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_NetworkOut_count", + "unit": "Bytes", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_NetworkOut_max", + "unit": "Bytes", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_NetworkOut_min", + "unit": "Bytes", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_NetworkOut_sum", + "unit": "Bytes", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_NetworkPacketsIn_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_NetworkPacketsIn_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_NetworkPacketsIn_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_NetworkPacketsIn_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_NetworkPacketsOut_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_NetworkPacketsOut_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_NetworkPacketsOut_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_NetworkPacketsOut_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_StatusCheckFailed_AttachedEBS_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_StatusCheckFailed_AttachedEBS_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_StatusCheckFailed_AttachedEBS_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_StatusCheckFailed_AttachedEBS_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_StatusCheckFailed_Instance_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_StatusCheckFailed_Instance_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_StatusCheckFailed_Instance_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_StatusCheckFailed_Instance_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_StatusCheckFailed_System_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_StatusCheckFailed_System_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_StatusCheckFailed_System_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_StatusCheckFailed_System_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_StatusCheckFailed_count", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_StatusCheckFailed_max", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_StatusCheckFailed_min", + "unit": "Count", + "type": "Gauge", + "description": "" + }, + { + "name": "aws_EC2_StatusCheckFailed_sum", + "unit": "Count", + "type": "Gauge", + "description": "" + } + ], + "logs": [] + }, + "telemetryCollectionStrategy": { + "aws": { + "metrics": { + "cloudwatchMetricStreamFilters": [ + { + "Namespace": "AWS/EC2" + }, + { + "Namespace": "CWAgent" + } + ] + } + } + }, + "assets": { + "dashboards": [ + { + "id": "overview", + "title": "EC2 Overview", + "description": "Overview of EC2", + "definition": "file://assets/dashboards/overview.json" + } + ] + } +} diff --git a/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ec2/overview.md b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ec2/overview.md new file mode 100644 index 00000000000..3a1642c016e --- /dev/null +++ b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ec2/overview.md @@ -0,0 +1,3 @@ +### Monitor EC2 with SigNoz + +Collect key EC2 metrics and view them with an out of the box dashboard. diff --git a/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ecs/assets/dashboards/containerinsights.json b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ecs/assets/dashboards/containerinsights.json new file mode 100644 index 00000000000..f351d26127b --- /dev/null +++ b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ecs/assets/dashboards/containerinsights.json @@ -0,0 +1,1965 @@ +{ + "description": "View key ECS ContainerInsights metrics with an out of the box dashboard.", + "image":"data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%3F%3E%3Csvg%20width%3D%2280px%22%20height%3D%2280px%22%20viewBox%3D%220%200%2080%2080%22%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%3E%3C!--%20Generator%3A%20Sketch%2064%20(93537)%20-%20https%3A%2F%2Fsketch.com%20--%3E%3Ctitle%3EIcon-Architecture%2F64%2FArch_Amazon-Elastic-Container-Service_64%3C%2Ftitle%3E%3Cdesc%3ECreated%20with%20Sketch.%3C%2Fdesc%3E%3Cdefs%3E%3ClinearGradient%20x1%3D%220%25%22%20y1%3D%22100%25%22%20x2%3D%22100%25%22%20y2%3D%220%25%22%20id%3D%22linearGradient-1%22%3E%3Cstop%20stop-color%3D%22%23C8511B%22%20offset%3D%220%25%22%3E%3C%2Fstop%3E%3Cstop%20stop-color%3D%22%23FF9900%22%20offset%3D%22100%25%22%3E%3C%2Fstop%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Cg%20id%3D%22Icon-Architecture%2F64%2FArch_Amazon-Elastic-Container-Service_64%22%20stroke%3D%22none%22%20stroke-width%3D%221%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Cg%20id%3D%22Icon-Architecture-BG%2F64%2FContainers%22%20fill%3D%22url(%23linearGradient-1)%22%3E%3Crect%20id%3D%22Rectangle%22%20x%3D%220%22%20y%3D%220%22%20width%3D%2280%22%20height%3D%2280%22%3E%3C%2Frect%3E%3C%2Fg%3E%3Cpath%20d%3D%22M64%2C48.2340095%20L56%2C43.4330117%20L56%2C32.0000169%20C56%2C31.6440171%2055.812%2C31.3150172%2055.504%2C31.1360173%20L44%2C24.4260204%20L44%2C14.7520248%20L64%2C26.5710194%20L64%2C48.2340095%20Z%20M65.509%2C25.13902%20L43.509%2C12.139026%20C43.199%2C11.9560261%2042.818%2C11.9540261%2042.504%2C12.131026%20C42.193%2C12.3090259%2042%2C12.6410257%2042%2C13.0000256%20L42%2C25.0000201%20C42%2C25.3550199%2042.189%2C25.6840198%2042.496%2C25.8640197%20L54%2C32.5740166%20L54%2C44.0000114%20C54%2C44.3510113%2054.185%2C44.6770111%2054.486%2C44.857011%20L64.486%2C50.8570083%20C64.644%2C50.9520082%2064.822%2C51%2065%2C51%20C65.17%2C51%2065.34%2C50.9570082%2065.493%2C50.8700083%20C65.807%2C50.6930084%2066%2C50.3600085%2066%2C50%20L66%2C26.0000196%20C66%2C25.6460198%2065.814%2C25.31902%2065.509%2C25.13902%20L65.509%2C25.13902%20Z%20M40.445%2C66.863001%20L17%2C54.3990067%20L17%2C26.5710194%20L37%2C14.7520248%20L37%2C24.4510204%20L26.463%2C31.1560173%20C26.175%2C31.3400172%2026%2C31.6580171%2026%2C32.0000169%20L26%2C49.0000091%20C26%2C49.373009%2026.208%2C49.7150088%2026.538%2C49.8870087%20L39.991%2C56.8870055%20C40.28%2C57.0370055%2040.624%2C57.0380055%2040.912%2C56.8880055%20L53.964%2C50.1440086%20L61.996%2C54.9640064%20L40.445%2C66.863001%20Z%20M64.515%2C54.1420068%20L54.515%2C48.1420095%20C54.217%2C47.9640096%2053.849%2C47.9520096%2053.541%2C48.1120095%20L40.455%2C54.8730065%20L28%2C48.3930094%20L28%2C32.5490167%20L38.537%2C25.8440197%20C38.825%2C25.6600198%2039%2C25.3420199%2039%2C25.0000201%20L39%2C13.0000256%20C39%2C12.6410257%2038.808%2C12.3090259%2038.496%2C12.131026%20C38.184%2C11.9540261%2037.802%2C11.9560261%2037.491%2C12.139026%20L15.491%2C25.13902%20C15.187%2C25.31902%2015%2C25.6460198%2015%2C26.0000196%20L15%2C55%20C15%2C55.3690062%2015.204%2C55.7090061%2015.53%2C55.883006%20L39.984%2C68.8830001%20C40.131%2C68.961%2040.292%2C69%2040.453%2C69%20C40.62%2C69%2040.786%2C68.958%2040.937%2C68.8750001%20L64.484%2C55.875006%20C64.797%2C55.7020061%2064.993%2C55.3750062%2065.0001416%2C55.0180064%20C65.006%2C54.6600066%2064.821%2C54.3260067%2064.515%2C54.1420068%20L64.515%2C54.1420068%20Z%22%20id%3D%22Amazon-Elastic-Container-Service_Icon_64_Squid%22%20fill%3D%22%23FFFFFF%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E", + "layout": [ + { + "h": 3, + "i": "02e56dd4-5c3d-4a17-9efb-9d250fdda4c3", + "moved": false, + "static": false, + "w": 3, + "x": 0, + "y": 0 + }, + { + "h": 3, + "i": "915aa67d-6ac1-4e52-9b5b-e41156273f31", + "moved": false, + "static": false, + "w": 3, + "x": 3, + "y": 0 + }, + { + "h": 3, + "i": "854acc58-84b1-49de-b4e9-13f5a01b1e2e", + "moved": false, + "static": false, + "w": 3, + "x": 6, + "y": 0 + }, + { + "h": 3, + "i": "268f968a-0a0f-47d5-9bf0-1095c5bd9057", + "moved": false, + "static": false, + "w": 3, + "x": 9, + "y": 0 + }, + { + "h": 6, + "i": "5bea2bc0-13a2-4937-bccb-60ffe8a43ad5", + "moved": false, + "static": false, + "w": 6, + "x": 0, + "y": 3 + }, + { + "h": 6, + "i": "f78becf8-0328-48b4-84b6-ff4dac325940", + "moved": false, + "static": false, + "w": 6, + "x": 6, + "y": 3 + }, + { + "h": 6, + "i": "7e83cf03-4886-46e1-ab77-3ceb61618f6c", + "w": 6, + "x": 0, + "y": 9 + }, + { + "h": 6, + "i": "324f5c5b-d565-48fa-af80-5c722ddd4bc7", + "w": 6, + "x": 6, + "y": 9 + }, + { + "h": 6, + "i": "8f0c9479-61ec-49de-b4b5-94dd69f7bb53", + "w": 6, + "x": 0, + "y": 15 + }, + { + "h": 6, + "i": "c0be2002-efee-4757-9f1e-c16ad6e9a612", + "w": 6, + "x": 6, + "y": 15 + } + ], + "panelMap": {}, + "tags": [], + "title": "ECS ContainerInsights", + "uploadedGrafana": false, + "variables": { + "51f4fa2b-89c7-47c2-9795-f32cffaab985": { + "allSelected": false, + "customValue": "", + "description": "AWS Account ID", + "id": "51f4fa2b-89c7-47c2-9795-f32cffaab985", + "key": "51f4fa2b-89c7-47c2-9795-f32cffaab985", + "modificationUUID": "7b814d17-8fff-4ed6-a4ea-90e3b1a97584", + "multiSelect": false, + "name": "Account", + "order": 0, + "queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud_account_id') AS cloud_account_id\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_ECS_MemoryUtilization_max' GROUP BY cloud_account_id", + "showALLOption": false, + "sort": "DISABLED", + "textboxValue": "", + "type": "QUERY" + }, + "9faf0f4b-b245-4b3c-83a3-60cfa76dfeb0": { + "allSelected": false, + "customValue": "", + "description": "Account Region", + "id": "9faf0f4b-b245-4b3c-83a3-60cfa76dfeb0", + "key": "9faf0f4b-b245-4b3c-83a3-60cfa76dfeb0", + "modificationUUID": "3b5f499b-22a3-4c8a-847c-8d3811c9e6b2", + "multiSelect": false, + "name": "Region", + "order": 1, + "queryValue": "SELECT DISTINCT JSONExtractString(labels, 'cloud_region') AS region\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'aws_ECS_MemoryUtilization_max' AND JSONExtractString(labels, 'cloud_account_id') IN {{.Account}} GROUP BY region", + "showALLOption": false, + "sort": "ASC", + "textboxValue": "", + "type": "QUERY" + }, + "bfbdbcbe-a168-4d81-b108-36339e249116": { + "allSelected": false, + "customValue": "", + "description": "ECS Cluster Name", + "id": "bfbdbcbe-a168-4d81-b108-36339e249116", + "key": "bfbdbcbe-a168-4d81-b108-36339e249116", + "modificationUUID": "917c531b-9dfe-4576-8d35-5ebc78966be3", + "multiSelect": false, + "name": "Cluster", + "order": 2, + "queryValue": "SELECT DISTINCT JSONExtractString(labels, 'ClusterName') AS cluster\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name like '%aws_ECS%' AND JSONExtractString(labels, 'cloud_account_id') IN {{.Account}} AND JSONExtractString(labels, 'cloud_region') IN {{.Region}}\nGROUP BY cluster", + "showALLOption": false, + "sort": "ASC", + "textboxValue": "", + "type": "QUERY" + } + }, + "version": "v4", + "widgets": [ + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "customLegendColors": {}, + "description": "", + "fillSpans": false, + "id": "f78becf8-0328-48b4-84b6-ff4dac325940", + "isLogScale": false, + "isStacked": false, + "legendPosition": "bottom", + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ECS_MemoryUtilization_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ECS_MemoryUtilization_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "c002d3ea", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "d95dc93f", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "7eb7332c", + "key": { + "dataType": "string", + "id": "ClusterName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ClusterName", + "type": "tag" + }, + "op": "=", + "value": "$Cluster" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ServiceName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ServiceName", + "type": "tag" + } + ], + "having": [], + "legend": "{{ServiceName}} ", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "621e32fc-8ad3-44b7-8512-e5c58f39b791", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Memory Utilization", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "customLegendColors": {}, + "description": "", + "fillSpans": false, + "id": "5bea2bc0-13a2-4937-bccb-60ffe8a43ad5", + "isLogScale": false, + "isStacked": false, + "legendPosition": "bottom", + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ECS_CPUUtilization_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ECS_CPUUtilization_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "8ae50256", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "dada2be4", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "adaf587a", + "key": { + "dataType": "string", + "id": "ClusterName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ClusterName", + "type": "tag" + }, + "op": "=", + "value": "$Cluster" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ServiceName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ServiceName", + "type": "tag" + } + ], + "having": [], + "legend": "{{ServiceName}} ", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "edb1997e-44a5-4326-ac18-a7a87e0c43e6", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "CPU Utilization", + "yAxisUnit": "none" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "customLegendColors": {}, + "description": "", + "fillSpans": false, + "id": "02e56dd4-5c3d-4a17-9efb-9d250fdda4c3", + "isLogScale": false, + "isStacked": false, + "legendPosition": "bottom", + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "value", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ECS_ContainerInsights_CpuUtilized_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ECS_ContainerInsights_CpuUtilized_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "840f6a82", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "e494eace", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "d615bfdb", + "key": { + "dataType": "string", + "id": "ClusterName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ClusterName", + "type": "tag" + }, + "op": "in", + "value": [ + "$Cluster" + ] + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [], + "having": [], + "legend": "", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "83556bb4-a779-4450-bb1b-2a20ae63dc90", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "CPU Usage", + "yAxisUnit": "percent" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "customLegendColors": {}, + "description": "", + "fillSpans": false, + "id": "915aa67d-6ac1-4e52-9b5b-e41156273f31", + "isLogScale": false, + "isStacked": false, + "legendPosition": "bottom", + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "value", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ECS_ContainerInsights_CpuReserved_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ECS_ContainerInsights_CpuReserved_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "98cf55a2", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "dc2591e8", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "5fe36864", + "key": { + "dataType": "", + "isColumn": false, + "key": "ClusterName", + "type": "" + }, + "op": "=", + "value": "$Cluster" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [], + "having": [], + "legend": "", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "10baab3b-a8de-4f23-835c-b4e5e099b5dc", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "CPU Reserved", + "yAxisUnit": "percent" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "customLegendColors": {}, + "description": "", + "fillSpans": false, + "id": "854acc58-84b1-49de-b4e9-13f5a01b1e2e", + "isLogScale": false, + "isStacked": false, + "legendPosition": "bottom", + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "value", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ECS_ContainerInsights_MemoryUtilized_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ECS_ContainerInsights_MemoryUtilized_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "6d3fb70d", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "763ec68f", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "69a793fb", + "key": { + "dataType": "", + "isColumn": false, + "key": "ClusterName", + "type": "" + }, + "op": "=", + "value": "$Cluster" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [], + "having": [], + "legend": "", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "03e8fd1f-33e2-40ee-bb0a-f385a26794db", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Memory Usage", + "yAxisUnit": "percent" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "customLegendColors": {}, + "description": "", + "fillSpans": false, + "id": "268f968a-0a0f-47d5-9bf0-1095c5bd9057", + "isLogScale": false, + "isStacked": false, + "legendPosition": "bottom", + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "value", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ECS_ContainerInsights_MemoryReserved_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ECS_ContainerInsights_MemoryReserved_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "4cabe614", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "077e09db", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "71f30fef", + "key": { + "dataType": "", + "isColumn": false, + "key": "ClusterName", + "type": "" + }, + "op": "=", + "value": "$Cluster" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [], + "having": [], + "legend": "", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "be0eeb73-0f56-4e74-9add-e822c16bf8dc", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Memory Reserved", + "yAxisUnit": "percent" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "customLegendColors": {}, + "description": "", + "fillSpans": false, + "id": "7e83cf03-4886-46e1-ab77-3ceb61618f6c", + "isLogScale": false, + "isStacked": false, + "legendPosition": "bottom", + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ECS_ContainerInsights_NetworkRxBytes_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ECS_ContainerInsights_NetworkRxBytes_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "8e15b10c", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "92d56544", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "7c5e0300", + "key": { + "dataType": "string", + "id": "ClusterName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ClusterName", + "type": "tag" + }, + "op": "=", + "value": "$Cluster" + }, + { + "id": "f41089a9", + "key": { + "dataType": "string", + "id": "ServiceName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ServiceName", + "type": "tag" + }, + "op": "exists", + "value": "" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ServiceName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ServiceName", + "type": "tag" + } + ], + "having": [], + "legend": "{{ServiceName}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "70643e69-8796-4a33-a515-c3f6a9bf461a", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Network IO (Receive)", + "yAxisUnit": "Bps" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "customLegendColors": {}, + "description": "", + "fillSpans": false, + "id": "324f5c5b-d565-48fa-af80-5c722ddd4bc7", + "isLogScale": false, + "isStacked": false, + "legendPosition": "bottom", + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ECS_ContainerInsights_NetworkTxBytes_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ECS_ContainerInsights_NetworkTxBytes_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "6a1059e5", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "fe0d40de", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "b6047f19", + "key": { + "dataType": "string", + "id": "ClusterName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ClusterName", + "type": "tag" + }, + "op": "=", + "value": "$Cluster" + }, + { + "id": "8e76d8f1", + "key": { + "dataType": "string", + "id": "ServiceName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ServiceName", + "type": "tag" + }, + "op": "exists", + "value": "" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ServiceName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ServiceName", + "type": "tag" + } + ], + "having": [], + "legend": "{{ServiceName}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "392574c7-aa8d-42f1-8bf1-0a42c0f7448e", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Network IO (Transmit)", + "yAxisUnit": "Bps" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "customLegendColors": {}, + "description": "", + "fillSpans": false, + "id": "8f0c9479-61ec-49de-b4b5-94dd69f7bb53", + "isLogScale": false, + "isStacked": false, + "legendPosition": "bottom", + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ECS_ContainerInsights_StorageReadBytes_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ECS_ContainerInsights_StorageReadBytes_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "89f0e499", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "91ce3091", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "50782f6c", + "key": { + "dataType": "string", + "id": "ClusterName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ClusterName", + "type": "tag" + }, + "op": "=", + "value": "$Cluster" + }, + { + "id": "6eeed520", + "key": { + "dataType": "string", + "id": "ServiceName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ServiceName", + "type": "tag" + }, + "op": "exists", + "value": "" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ClusterName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ClusterName", + "type": "tag" + }, + { + "dataType": "string", + "id": "ServiceName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ServiceName", + "type": "tag" + } + ], + "having": [], + "legend": "{{ServiceName}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "15e91871-0611-4b3c-b647-ace2c54e41e1", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Storage Read", + "yAxisUnit": "Bps" + }, + { + "bucketCount": 30, + "bucketWidth": 0, + "columnUnits": {}, + "customLegendColors": {}, + "description": "", + "fillSpans": false, + "id": "c0be2002-efee-4757-9f1e-c16ad6e9a612", + "isLogScale": false, + "isStacked": false, + "legendPosition": "bottom", + "mergeAllActiveQueries": false, + "nullZeroValues": "zero", + "opacity": "1", + "panelTypes": "graph", + "query": { + "builder": { + "queryData": [ + { + "aggregateAttribute": { + "dataType": "float64", + "id": "aws_ECS_ContainerInsights_StorageWriteBytes_max--float64--Gauge--true", + "isColumn": true, + "isJSON": false, + "key": "aws_ECS_ContainerInsights_StorageWriteBytes_max", + "type": "Gauge" + }, + "aggregateOperator": "max", + "dataSource": "metrics", + "disabled": false, + "expression": "A", + "filters": { + "items": [ + { + "id": "edef4331", + "key": { + "dataType": "string", + "id": "cloud_region--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_region", + "type": "tag" + }, + "op": "=", + "value": "$Region" + }, + { + "id": "d6081c36", + "key": { + "dataType": "string", + "id": "cloud_account_id--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "cloud_account_id", + "type": "tag" + }, + "op": "=", + "value": "$Account" + }, + { + "id": "7ae5a9e4", + "key": { + "dataType": "string", + "id": "ClusterName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ClusterName", + "type": "tag" + }, + "op": "=", + "value": "$Cluster" + }, + { + "id": "5f7feeb2", + "key": { + "dataType": "string", + "id": "ServiceName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ServiceName", + "type": "tag" + }, + "op": "exists", + "value": "" + } + ], + "op": "AND" + }, + "functions": [], + "groupBy": [ + { + "dataType": "string", + "id": "ServiceName--string--tag--false", + "isColumn": false, + "isJSON": false, + "key": "ServiceName", + "type": "tag" + } + ], + "having": [], + "legend": "{{ServiceName}}", + "limit": null, + "orderBy": [], + "queryName": "A", + "reduceTo": "avg", + "spaceAggregation": "max", + "stepInterval": 60, + "timeAggregation": "max" + } + ], + "queryFormulas": [] + }, + "clickhouse_sql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "id": "8485a05c-5251-4d42-8a8c-bb5a2691fc9d", + "promql": [ + { + "disabled": false, + "legend": "", + "name": "A", + "query": "" + } + ], + "queryType": "builder" + }, + "selectedLogFields": [ + { + "dataType": "string", + "name": "body", + "type": "" + }, + { + "dataType": "string", + "name": "timestamp", + "type": "" + } + ], + "selectedTracesFields": [ + { + "dataType": "string", + "id": "serviceName--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "serviceName", + "type": "tag" + }, + { + "dataType": "string", + "id": "name--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "name", + "type": "tag" + }, + { + "dataType": "float64", + "id": "durationNano--float64--tag--true", + "isColumn": true, + "isJSON": false, + "key": "durationNano", + "type": "tag" + }, + { + "dataType": "string", + "id": "httpMethod--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "httpMethod", + "type": "tag" + }, + { + "dataType": "string", + "id": "responseStatusCode--string--tag--true", + "isColumn": true, + "isJSON": false, + "key": "responseStatusCode", + "type": "tag" + } + ], + "softMax": 0, + "softMin": 0, + "stackedBarChart": false, + "thresholds": [], + "timePreferance": "GLOBAL_TIME", + "title": "Storage Write", + "yAxisUnit": "Bps" + } + ] +} \ No newline at end of file diff --git a/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ecs/assets/dashboards/containerinsights.png b/pkg/modules/cloudintegration/implcloudintegration/fs/definitions/aws/ecs/assets/dashboards/containerinsights.png new file mode 100644 index 0000000000000000000000000000000000000000..1bc6b58d3d2e1a75a06e28c7c6b1950ab840c7e3 GIT binary patch literal 401276 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

diff --git a/frontend/src/container/SideNav/SideNav.tsx b/frontend/src/container/SideNav/SideNav.tsx index afc8a2895b2..34a22bc55bb 100644 --- a/frontend/src/container/SideNav/SideNav.tsx +++ b/frontend/src/container/SideNav/SideNav.tsx @@ -695,6 +695,15 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element { registerShortcut(GlobalShortcuts.NavigateToSettingsNotificationChannels, () => onClickHandler(ROUTES.ALL_CHANNELS, null), ); + registerShortcut(GlobalShortcuts.NavigateToSettingsServiceAccounts, () => + onClickHandler(ROUTES.SERVICE_ACCOUNTS_SETTINGS, null), + ); + registerShortcut(GlobalShortcuts.NavigateToSettingsRoles, () => + onClickHandler(ROUTES.ROLES_SETTINGS, null), + ); + registerShortcut(GlobalShortcuts.NavigateToSettingsMembers, () => + onClickHandler(ROUTES.MEMBERS_SETTINGS, null), + ); registerShortcut(GlobalShortcuts.NavigateToLogsPipelines, () => onClickHandler(ROUTES.LOGS_PIPELINES, null), ); @@ -718,6 +727,9 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element { deregisterShortcut(GlobalShortcuts.NavigateToSettingsIngestion); deregisterShortcut(GlobalShortcuts.NavigateToSettingsBilling); deregisterShortcut(GlobalShortcuts.NavigateToSettingsNotificationChannels); + deregisterShortcut(GlobalShortcuts.NavigateToSettingsServiceAccounts); + deregisterShortcut(GlobalShortcuts.NavigateToSettingsRoles); + deregisterShortcut(GlobalShortcuts.NavigateToSettingsMembers); deregisterShortcut(GlobalShortcuts.NavigateToLogsPipelines); deregisterShortcut(GlobalShortcuts.NavigateToLogsViews); deregisterShortcut(GlobalShortcuts.NavigateToTracesViews); diff --git a/frontend/src/pages/Settings/Settings.tsx b/frontend/src/pages/Settings/Settings.tsx index 57611f5479b..39d79b64a3f 100644 --- a/frontend/src/pages/Settings/Settings.tsx +++ b/frontend/src/pages/Settings/Settings.tsx @@ -143,7 +143,9 @@ function SettingsPage(): JSX.Element { isEnabled: item.key === ROUTES.ORG_SETTINGS || item.key === ROUTES.MEMBERS_SETTINGS || - item.key === ROUTES.SERVICE_ACCOUNTS_SETTINGS + item.key === ROUTES.SERVICE_ACCOUNTS_SETTINGS || + item.key === ROUTES.ROLES_SETTINGS || + item.key === ROUTES.ROLE_DETAILS ? true : item.isEnabled, })); diff --git a/frontend/src/pages/Settings/utils.ts b/frontend/src/pages/Settings/utils.ts index 9521f95cc7e..c472abef175 100644 --- a/frontend/src/pages/Settings/utils.ts +++ b/frontend/src/pages/Settings/utils.ts @@ -62,12 +62,16 @@ export const getRoutes = ( settings.push(...alertChannels(t)); if (isAdmin) { - settings.push(...membersSettings(t), ...serviceAccountsSettings(t)); + settings.push( + ...membersSettings(t), + ...serviceAccountsSettings(t), + ...rolesSettings(t), + ...roleDetails(t), + ); } - // todo: Sagar - check the condition for role list and details page, to whom we want to serve if ((isCloudUser || isEnterpriseSelfHostedUser) && isAdmin) { - settings.push(...billingSettings(t), ...rolesSettings(t), ...roleDetails(t)); + settings.push(...billingSettings(t)); } settings.push( From 419bd60a41015520f9be36ea108fdab66c0f58e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vinicius=20Louren=C3=A7o?= <12551007+H4ad@users.noreply.github.com> Date: Thu, 2 Apr 2026 08:17:49 -0300 Subject: [PATCH 76/78] feat(global-time-adapter): start migration away from redux (#10780) * feat(global-time-adapter): start migration away from redux * test(constant): add missing constants * refactor(hooks): move to hooks folder --- .../GlobalTimeStoreAdapter.tsx | 52 ++++ .../__tests__/GlobalTimeStoreAdapter.test.tsx | 227 ++++++++++++++++++ frontend/src/constants/reactQueryKeys.ts | 7 + .../TopNav/DateTimeSelectionV2/index.tsx | 13 +- frontend/src/hooks/globalTime/index.ts | 2 + .../useGlobalTimeQueryInvalidate.ts | 16 ++ .../useIsGlobalTimeQueryRefreshing.ts | 13 + frontend/src/index.tsx | 2 + .../__tests__/globalTimeStore.test.ts | 204 ++++++++++++++++ .../store/globalTime/__tests__/utils.test.ts | 139 +++++++++++ .../src/store/globalTime/globalTimeStore.ts | 33 +++ frontend/src/store/globalTime/index.ts | 9 + frontend/src/store/globalTime/types.ts | 52 ++++ frontend/src/store/globalTime/utils.ts | 87 +++++++ 14 files changed, 855 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/GlobalTimeStoreAdapter/GlobalTimeStoreAdapter.tsx create mode 100644 frontend/src/components/GlobalTimeStoreAdapter/__tests__/GlobalTimeStoreAdapter.test.tsx create mode 100644 frontend/src/hooks/globalTime/index.ts create mode 100644 frontend/src/hooks/globalTime/useGlobalTimeQueryInvalidate.ts create mode 100644 frontend/src/hooks/globalTime/useIsGlobalTimeQueryRefreshing.ts create mode 100644 frontend/src/store/globalTime/__tests__/globalTimeStore.test.ts create mode 100644 frontend/src/store/globalTime/__tests__/utils.test.ts create mode 100644 frontend/src/store/globalTime/globalTimeStore.ts create mode 100644 frontend/src/store/globalTime/index.ts create mode 100644 frontend/src/store/globalTime/types.ts create mode 100644 frontend/src/store/globalTime/utils.ts diff --git a/frontend/src/components/GlobalTimeStoreAdapter/GlobalTimeStoreAdapter.tsx b/frontend/src/components/GlobalTimeStoreAdapter/GlobalTimeStoreAdapter.tsx new file mode 100644 index 00000000000..233bed8a4cf --- /dev/null +++ b/frontend/src/components/GlobalTimeStoreAdapter/GlobalTimeStoreAdapter.tsx @@ -0,0 +1,52 @@ +import { useEffect } from 'react'; +// eslint-disable-next-line no-restricted-imports +import { useSelector } from 'react-redux'; +import { refreshIntervalOptions } from 'container/TopNav/AutoRefreshV2/constants'; +import { Time } from 'container/TopNav/DateTimeSelectionV2/types'; +import { useGlobalTimeStore } from 'store/globalTime/globalTimeStore'; +import { createCustomTimeRange } from 'store/globalTime/utils'; +import { AppState } from 'store/reducers'; +import { GlobalReducer } from 'types/reducer/globalTime'; + +/** + * Adapter component that syncs Redux global time state to Zustand store. + * This component should be rendered once at the app level. + * + * It reads from the Redux globalTime reducer and updates the Zustand store + * to provide a migration path from Redux to Zustand. + */ +export function GlobalTimeStoreAdapter(): null { + const globalTime = useSelector( + (state) => state.globalTime, + ); + + const setSelectedTime = useGlobalTimeStore((s) => s.setSelectedTime); + + useEffect(() => { + // Convert the selectedTime to the new format + // If it's 'custom', store the min/max times in the custom format + const selectedTime = + globalTime.selectedTime === 'custom' + ? createCustomTimeRange(globalTime.minTime, globalTime.maxTime) + : (globalTime.selectedTime as Time); + + // Find refresh interval from Redux state + const refreshOption = refreshIntervalOptions.find( + (option) => option.key === globalTime.selectedAutoRefreshInterval, + ); + + const refreshInterval = + !globalTime.isAutoRefreshDisabled && refreshOption ? refreshOption.value : 0; + + setSelectedTime(selectedTime, refreshInterval); + }, [ + globalTime.selectedTime, + globalTime.isAutoRefreshDisabled, + globalTime.selectedAutoRefreshInterval, + globalTime.minTime, + globalTime.maxTime, + setSelectedTime, + ]); + + return null; +} diff --git a/frontend/src/components/GlobalTimeStoreAdapter/__tests__/GlobalTimeStoreAdapter.test.tsx b/frontend/src/components/GlobalTimeStoreAdapter/__tests__/GlobalTimeStoreAdapter.test.tsx new file mode 100644 index 00000000000..ff8ed915372 --- /dev/null +++ b/frontend/src/components/GlobalTimeStoreAdapter/__tests__/GlobalTimeStoreAdapter.test.tsx @@ -0,0 +1,227 @@ +// eslint-disable-next-line no-restricted-imports +import { Provider } from 'react-redux'; +import { act, render, renderHook } from '@testing-library/react'; +import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants'; +import configureStore, { MockStoreEnhanced } from 'redux-mock-store'; +import { useGlobalTimeStore } from 'store/globalTime/globalTimeStore'; +import { createCustomTimeRange } from 'store/globalTime/utils'; +import { AppState } from 'store/reducers'; +import { GlobalReducer } from 'types/reducer/globalTime'; + +import { GlobalTimeStoreAdapter } from '../GlobalTimeStoreAdapter'; + +const mockStore = configureStore>([]); + +const randomTime = 1700000000000000000; + +describe('GlobalTimeStoreAdapter', () => { + let store: MockStoreEnhanced>; + + const createGlobalTimeState = ( + overrides: Partial = {}, + ): GlobalReducer => ({ + minTime: randomTime, + maxTime: randomTime, + loading: false, + selectedTime: '15m', + isAutoRefreshDisabled: true, + selectedAutoRefreshInterval: 'off', + ...overrides, + }); + + beforeEach(() => { + // Reset Zustand store before each test + const { result } = renderHook(() => useGlobalTimeStore()); + act(() => { + result.current.setSelectedTime(DEFAULT_TIME_RANGE, 0); + }); + }); + + it('should render null because it just an adapter', () => { + store = mockStore({ + globalTime: createGlobalTimeState(), + }); + + const { container } = render( + + + , + ); + + expect(container.firstChild).toBeNull(); + }); + + it('should sync relative time from Redux to Zustand store', () => { + store = mockStore({ + globalTime: createGlobalTimeState({ + selectedTime: '15m', + isAutoRefreshDisabled: true, + selectedAutoRefreshInterval: 'off', + }), + }); + + render( + + + , + ); + + const { result } = renderHook(() => useGlobalTimeStore()); + expect(result.current.selectedTime).toBe('15m'); + expect(result.current.refreshInterval).toBe(0); + expect(result.current.isRefreshEnabled).toBe(false); + }); + + it('should sync custom time from Redux to Zustand store', () => { + store = mockStore({ + globalTime: createGlobalTimeState({ + selectedTime: 'custom', + minTime: randomTime, + maxTime: randomTime, + isAutoRefreshDisabled: true, + }), + }); + + render( + + + , + ); + + const { result } = renderHook(() => useGlobalTimeStore()); + expect(result.current.selectedTime).toBe( + createCustomTimeRange(randomTime, randomTime), + ); + expect(result.current.isRefreshEnabled).toBe(false); + }); + + it('should sync refresh interval when auto refresh is enabled', () => { + store = mockStore({ + globalTime: createGlobalTimeState({ + selectedTime: '15m', + isAutoRefreshDisabled: false, + selectedAutoRefreshInterval: '5s', + }), + }); + + render( + + + , + ); + + const { result } = renderHook(() => useGlobalTimeStore()); + expect(result.current.selectedTime).toBe('15m'); + expect(result.current.refreshInterval).toBe(5000); // 5s = 5000ms + expect(result.current.isRefreshEnabled).toBe(true); + }); + + it('should set refreshInterval to 0 when auto refresh is disabled', () => { + store = mockStore({ + globalTime: createGlobalTimeState({ + selectedTime: '15m', + isAutoRefreshDisabled: true, + selectedAutoRefreshInterval: '5s', // Even with interval set, should be 0 when disabled + }), + }); + + render( + + + , + ); + + const { result } = renderHook(() => useGlobalTimeStore()); + expect(result.current.refreshInterval).toBe(0); + expect(result.current.isRefreshEnabled).toBe(false); + }); + + it('should update Zustand store when Redux state changes', () => { + store = mockStore({ + globalTime: createGlobalTimeState({ + selectedTime: '15m', + isAutoRefreshDisabled: true, + }), + }); + + const { rerender } = render( + + + , + ); + + // Verify initial state + let zustandState = renderHook(() => useGlobalTimeStore()); + expect(zustandState.result.current.selectedTime).toBe('15m'); + + // Update Redux store + const newStore = mockStore({ + globalTime: createGlobalTimeState({ + selectedTime: '1h', + isAutoRefreshDisabled: false, + selectedAutoRefreshInterval: '30s', + }), + }); + + rerender( + + + , + ); + + // Verify updated state + zustandState = renderHook(() => useGlobalTimeStore()); + expect(zustandState.result.current.selectedTime).toBe('1h'); + expect(zustandState.result.current.refreshInterval).toBe(30000); // 30s = 30000ms + expect(zustandState.result.current.isRefreshEnabled).toBe(true); + }); + + it('should handle various refresh interval options', () => { + const testCases = [ + { key: '5s', expectedValue: 5000 }, + { key: '10s', expectedValue: 10000 }, + { key: '30s', expectedValue: 30000 }, + { key: '1m', expectedValue: 60000 }, + { key: '5m', expectedValue: 300000 }, + ]; + + testCases.forEach(({ key, expectedValue }) => { + store = mockStore({ + globalTime: createGlobalTimeState({ + selectedTime: '15m', + isAutoRefreshDisabled: false, + selectedAutoRefreshInterval: key, + }), + }); + + render( + + + , + ); + + const { result } = renderHook(() => useGlobalTimeStore()); + expect(result.current.refreshInterval).toBe(expectedValue); + }); + }); + + it('should handle unknown refresh interval by setting 0', () => { + store = mockStore({ + globalTime: createGlobalTimeState({ + selectedTime: '15m', + isAutoRefreshDisabled: false, + selectedAutoRefreshInterval: 'unknown-interval', + }), + }); + + render( + + + , + ); + + const { result } = renderHook(() => useGlobalTimeStore()); + expect(result.current.refreshInterval).toBe(0); + expect(result.current.isRefreshEnabled).toBe(false); + }); +}); diff --git a/frontend/src/constants/reactQueryKeys.ts b/frontend/src/constants/reactQueryKeys.ts index 89b3a60cf57..45f77cb0d4d 100644 --- a/frontend/src/constants/reactQueryKeys.ts +++ b/frontend/src/constants/reactQueryKeys.ts @@ -1,4 +1,11 @@ export const REACT_QUERY_KEY = { + /** + * For any query that should support AutoRefresh and min/max time is from DateTimeSelectionV2 + * You can prefix the query with this KEY, it will allow the queries to be automatically refreshed + * when the user clicks in the refresh button, or alert the user when the data is being refreshed. + */ + AUTO_REFRESH_QUERY: 'AUTO_REFRESH_QUERY', + GET_PUBLIC_DASHBOARD: 'GET_PUBLIC_DASHBOARD', GET_PUBLIC_DASHBOARD_META: 'GET_PUBLIC_DASHBOARD_META', GET_PUBLIC_DASHBOARD_WIDGET_DATA: 'GET_PUBLIC_DASHBOARD_WIDGET_DATA', diff --git a/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx b/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx index 6daa46c6582..c018395fd6e 100644 --- a/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx +++ b/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx @@ -14,6 +14,10 @@ import { QueryParams } from 'constants/query'; import ROUTES from 'constants/routes'; import NewExplorerCTA from 'container/NewExplorerCTA'; import dayjs, { Dayjs } from 'dayjs'; +import { + useGlobalTimeQueryInvalidate, + useIsGlobalTimeQueryRefreshing, +} from 'hooks/globalTime'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useSafeNavigate } from 'hooks/useSafeNavigate'; import useUrlQuery from 'hooks/useUrlQuery'; @@ -352,7 +356,10 @@ function DateTimeSelection({ ], ); + const isRefreshingQueries = useIsGlobalTimeQueryRefreshing(); + const invalidateQueries = useGlobalTimeQueryInvalidate(); const onRefreshHandler = (): void => { + invalidateQueries(); onSelectHandler(selectedTime); onLastRefreshHandler(); }; @@ -732,7 +739,11 @@ function DateTimeSelection({ {showAutoRefresh && selectedTime !== 'custom' && (
)} {selectedPanelType === PANEL_TYPES.TABLE && !showLiveLogs && ( - +
+ +
)}
diff --git a/frontend/src/pages/LogsExplorer/LogsExplorer.styles.scss b/frontend/src/pages/LogsExplorer/LogsExplorer.styles.scss index 82d3f5bffc7..effff3065c6 100644 --- a/frontend/src/pages/LogsExplorer/LogsExplorer.styles.scss +++ b/frontend/src/pages/LogsExplorer/LogsExplorer.styles.scss @@ -1,6 +1,9 @@ .logs-module-page { display: flex; height: 100%; + min-height: 0; + overflow: hidden; + .log-quick-filter-left-section { width: 0%; flex-shrink: 0; @@ -10,13 +13,19 @@ display: flex; flex-direction: column; width: 100%; + min-height: 0; + .log-explorer-query-container { display: flex; flex-direction: column; flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; .logs-explorer-views { flex: 1; + min-height: 0; display: flex; flex-direction: column; } @@ -26,6 +35,18 @@ &.filter-visible { .log-quick-filter-left-section { width: 260px; + height: 100%; + overflow: visible; + min-height: 0; + position: relative; + z-index: 2; + display: flex; + flex-direction: column; + + .quick-filters-container { + flex: 1; + min-height: 0; + } } .log-module-right-section { diff --git a/frontend/src/pages/LogsModulePage/LogsModulePage.styles.scss b/frontend/src/pages/LogsModulePage/LogsModulePage.styles.scss index acba2781dfb..d3874b1881f 100644 --- a/frontend/src/pages/LogsModulePage/LogsModulePage.styles.scss +++ b/frontend/src/pages/LogsModulePage/LogsModulePage.styles.scss @@ -1,10 +1,14 @@ .logs-module-container { flex: 1; + min-height: 0; display: flex; flex-direction: column; .ant-tabs { flex: 1; + min-height: 0; + display: flex; + flex-direction: column; } .ant-tabs-nav { @@ -18,14 +22,17 @@ .ant-tabs-content-holder { display: flex; + min-height: 0; .ant-tabs-content { flex: 1; + min-height: 0; display: flex; flex-direction: column; .ant-tabs-tabpane { flex: 1; + min-height: 0; display: flex; flex-direction: column; } diff --git a/frontend/yarn.lock b/frontend/yarn.lock index b004e5073f4..e10e981374d 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -6017,26 +6017,19 @@ dependencies: defer-to-connect "^2.0.0" -"@tanstack/react-table@8.20.6": - version "8.20.6" - resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.20.6.tgz#a1f3103327aa59aa621931f4087a7604a21054d0" - integrity sha512-w0jluT718MrOKthRcr2xsjqzx+oEM7B7s/XXyfs19ll++hlId3fjTm+B2zrR3ijpANpkzBAr15j1XGVOMxpggQ== - dependencies: - "@tanstack/table-core" "8.20.5" - -"@tanstack/react-table@^8.21.3": +"@tanstack/react-table@8.21.3", "@tanstack/react-table@^8.21.3": version "8.21.3" resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.21.3.tgz#2c38c747a5731c1a07174fda764b9c2b1fb5e91b" integrity sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww== dependencies: "@tanstack/table-core" "8.21.3" -"@tanstack/react-virtual@3.11.2": - version "3.11.2" - resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.11.2.tgz#d6b9bd999c181f0a2edce270c87a2febead04322" - integrity sha512-OuFzMXPF4+xZgx8UzJha0AieuMihhhaWG0tCqpp6tDzlFwOmNBPYMuLOtMJ1Tr4pXLHmgjcWhG6RlknY2oNTdQ== +"@tanstack/react-virtual@3.13.22": + version "3.13.22" + resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.13.22.tgz#9a5529dee4010f33272ae3b3e3728dee317b3b42" + integrity sha512-EaOrBBJLi3M0bTMQRjGkxLXRw7Gizwntoy5E2Q2UnSbML7Mo2a1P/Hfkw5tw9FLzK62bj34Jl6VNbQfRV6eJcA== dependencies: - "@tanstack/virtual-core" "3.11.2" + "@tanstack/virtual-core" "3.13.22" "@tanstack/react-virtual@^3.13.9": version "3.13.12" @@ -6045,26 +6038,21 @@ dependencies: "@tanstack/virtual-core" "3.13.12" -"@tanstack/table-core@8.20.5": - version "8.20.5" - resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.20.5.tgz#3974f0b090bed11243d4107283824167a395cf1d" - integrity sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg== - "@tanstack/table-core@8.21.3": version "8.21.3" resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.21.3.tgz#2977727d8fc8dfa079112d9f4d4c019110f1732c" integrity sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg== -"@tanstack/virtual-core@3.11.2": - version "3.11.2" - resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz#00409e743ac4eea9afe5b7708594d5fcebb00212" - integrity sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw== - "@tanstack/virtual-core@3.13.12": version "3.13.12" resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz#1dff176df9cc8f93c78c5e46bcea11079b397578" integrity sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA== +"@tanstack/virtual-core@3.13.22": + version "3.13.22" + resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.13.22.tgz#660a2cd048510125a4da898e5a659d53166f51af" + integrity sha512-isuUGKsc5TAPDoHSbWTbl1SCil54zOS2MiWz/9GCWHPUQOvNTQx8qJEWC7UWR0lShhbK0Lmkcf0SZYxvch7G3g== + "@testing-library/dom@^8.5.0": version "8.20.0" resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.0.tgz" From fec24c2cc37fe5ce371754ea00ee03133757824a Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Fri, 3 Apr 2026 15:37:38 +0530 Subject: [PATCH 78/78] fix(authz): better retry loop for roles (#10821) --- pkg/modules/serviceaccount/implserviceaccount/module.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/modules/serviceaccount/implserviceaccount/module.go b/pkg/modules/serviceaccount/implserviceaccount/module.go index 01e780190f7..8f1d1c29780 100644 --- a/pkg/modules/serviceaccount/implserviceaccount/module.go +++ b/pkg/modules/serviceaccount/implserviceaccount/module.go @@ -144,12 +144,12 @@ func (module *module) DeleteRole(ctx context.Context, orgID valuer.UUID, id valu return err } - err = module.store.DeleteServiceAccountRole(ctx, serviceAccount.ID, roleID) + err = module.authz.Revoke(ctx, orgID, []string{role.Name}, authtypes.MustNewSubject(authtypes.TypeableServiceAccount, id.String(), orgID, nil)) if err != nil { return err } - err = module.authz.Revoke(ctx, orgID, []string{role.Name}, authtypes.MustNewSubject(authtypes.TypeableServiceAccount, id.String(), orgID, nil)) + err = module.store.DeleteServiceAccountRole(ctx, serviceAccount.ID, roleID) if err != nil { return err } @@ -386,12 +386,12 @@ func (module *module) setRole(ctx context.Context, orgID valuer.UUID, id valuer. return err } - err = module.store.CreateServiceAccountRole(ctx, serviceAccountRole) + err = module.authz.Grant(ctx, orgID, []string{role.Name}, authtypes.MustNewSubject(authtypes.TypeableServiceAccount, id.String(), orgID, nil)) if err != nil { return err } - err = module.authz.Grant(ctx, orgID, []string{role.Name}, authtypes.MustNewSubject(authtypes.TypeableServiceAccount, id.String(), orgID, nil)) + err = module.store.CreateServiceAccountRole(ctx, serviceAccountRole) if err != nil { return err }