mirror of
https://github.com/SigNoz/signoz.git
synced 2026-09-19 09:50:41 +01:00
Compare commits
3 Commits
refactor/u
...
fix/duplic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bddd80ad9f | ||
|
|
82bbc35acd | ||
|
|
2aaa2a8158 |
14
.github/pull_request_template.md
vendored
14
.github/pull_request_template.md
vendored
@@ -11,20 +11,6 @@
|
||||
|
||||
---
|
||||
|
||||
|
||||
## 📝 Changelog
|
||||
|
||||
> Fill this only if the change affects users, APIs, UI, or documented behavior.
|
||||
Mention as N/A for internal refactors or non-user-visible changes.
|
||||
|
||||
**Deployment Type:** Cloud / OSS / Enterprise
|
||||
|
||||
**Type:** Feature / Bug Fix / Maintenance
|
||||
|
||||
**Description:** Short, user-facing summary of the change
|
||||
|
||||
---
|
||||
|
||||
## 🏷️ Required: Add Relevant Labels
|
||||
|
||||
> ⚠️ **Manually add appropriate labels in the PR sidebar**
|
||||
|
||||
1
.github/workflows/integrationci.yaml
vendored
1
.github/workflows/integrationci.yaml
vendored
@@ -51,7 +51,6 @@ jobs:
|
||||
- sqlite
|
||||
clickhouse-version:
|
||||
- 25.5.6
|
||||
- 25.10.1
|
||||
schema-migrator-version:
|
||||
- v0.129.7
|
||||
postgres-version:
|
||||
|
||||
17
Makefile
17
Makefile
@@ -210,23 +210,6 @@ py-lint: ## Run lint for integration tests
|
||||
@cd tests/integration && poetry run autoflake .
|
||||
@cd tests/integration && poetry run pylint .
|
||||
|
||||
.PHONY: py-test-setup
|
||||
py-test-setup: ## Runs integration tests
|
||||
@cd tests/integration && poetry run pytest --basetemp=./tmp/ -vv --reuse --capture=no src/bootstrap/setup.py::test_setup
|
||||
|
||||
.PHONY: py-test-teardown
|
||||
py-test-teardown: ## Runs integration tests with teardown
|
||||
@cd tests/integration && poetry run pytest --basetemp=./tmp/ -vv --teardown --capture=no src/bootstrap/setup.py::test_teardown
|
||||
|
||||
.PHONY: py-test
|
||||
py-test: ## Runs integration tests
|
||||
@cd tests/integration && poetry run pytest --basetemp=./tmp/ -vv --capture=no src/
|
||||
|
||||
.PHONY: py-clean
|
||||
py-clean: ## Clear all pycache and pytest cache from tests directory recursively
|
||||
@echo ">> cleaning python cache files from tests directory"
|
||||
@find tests -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
|
||||
@find tests -type d -name .pytest_cache -exec rm -rf {} + 2>/dev/null || true
|
||||
@find tests -type f -name "*.pyc" -delete 2>/dev/null || true
|
||||
@find tests -type f -name "*.pyo" -delete 2>/dev/null || true
|
||||
@echo ">> python cache cleaned"
|
||||
|
||||
97
cmd/zap.go
97
cmd/zap.go
@@ -1,10 +1,6 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"go.uber.org/zap" //nolint:depguard
|
||||
"go.uber.org/zap/zapcore" //nolint:depguard
|
||||
)
|
||||
@@ -14,97 +10,6 @@ func newZapLogger() *zap.Logger {
|
||||
config := zap.NewProductionConfig()
|
||||
config.EncoderConfig.TimeKey = "timestamp"
|
||||
config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
|
||||
|
||||
// Extract sampling config before building the logger.
|
||||
// We need to disable sampling in the config and apply it manually later
|
||||
// to ensure correct core ordering. See filteringCore documentation for details.
|
||||
samplerConfig := config.Sampling
|
||||
config.Sampling = nil
|
||||
|
||||
logger, _ := config.Build()
|
||||
|
||||
// Wrap with custom core wrapping to filter certain log entries.
|
||||
// The order of wrapping is important:
|
||||
// 1. First wrap with filteringCore
|
||||
// 2. Then wrap with sampler
|
||||
//
|
||||
// This creates the call chain: sampler -> filteringCore -> ioCore
|
||||
//
|
||||
// During logging:
|
||||
// - sampler.Check decides whether to sample the log entry
|
||||
// - If sampled, filteringCore.Check is called
|
||||
// - filteringCore adds itself to CheckedEntry.cores
|
||||
// - All cores in CheckedEntry.cores have their Write method called
|
||||
// - filteringCore.Write can now filter the entry before passing to ioCore
|
||||
//
|
||||
// If we didn't disable the sampler above, filteringCore would have wrapped
|
||||
// sampler. By calling sampler.Check we would have allowed it to call
|
||||
// ioCore.Check that adds itself to CheckedEntry.cores. Then ioCore.Write
|
||||
// would have bypassed our checks, making filtering impossible.
|
||||
return logger.WithOptions(zap.WrapCore(func(core zapcore.Core) zapcore.Core {
|
||||
core = &filteringCore{core}
|
||||
if samplerConfig != nil {
|
||||
core = zapcore.NewSamplerWithOptions(
|
||||
core,
|
||||
time.Second,
|
||||
samplerConfig.Initial,
|
||||
samplerConfig.Thereafter,
|
||||
)
|
||||
}
|
||||
return core
|
||||
}))
|
||||
}
|
||||
|
||||
// filteringCore wraps a zapcore.Core to filter out log entries based on a
|
||||
// custom logic.
|
||||
//
|
||||
// Note: This core must be positioned before the sampler in the core chain
|
||||
// to ensure Write is called. See newZapLogger for ordering details.
|
||||
type filteringCore struct {
|
||||
zapcore.Core
|
||||
}
|
||||
|
||||
// filter determines whether a log entry should be written based on its fields.
|
||||
// Returns false if the entry should be suppressed, true otherwise.
|
||||
//
|
||||
// Current filters:
|
||||
// - context.Canceled: These are expected errors from cancelled operations,
|
||||
// and create noise in logs.
|
||||
func (c *filteringCore) filter(fields []zapcore.Field) bool {
|
||||
for _, field := range fields {
|
||||
if field.Type == zapcore.ErrorType {
|
||||
if loggedErr, ok := field.Interface.(error); ok {
|
||||
// Suppress logs containing context.Canceled errors
|
||||
if errors.Is(loggedErr, context.Canceled) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// With implements zapcore.Core.With
|
||||
// It returns a new copy with the added context.
|
||||
func (c *filteringCore) With(fields []zapcore.Field) zapcore.Core {
|
||||
return &filteringCore{c.Core.With(fields)}
|
||||
}
|
||||
|
||||
// Check implements zapcore.Core.Check.
|
||||
// It adds this core to the CheckedEntry if the log level is enabled,
|
||||
// ensuring that Write will be called for this entry.
|
||||
func (c *filteringCore) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
|
||||
if c.Enabled(ent.Level) {
|
||||
return ce.AddCore(ent, c)
|
||||
}
|
||||
return ce
|
||||
}
|
||||
|
||||
// Write implements zapcore.Core.Write.
|
||||
// It filters log entries based on their fields before delegating to the wrapped core.
|
||||
func (c *filteringCore) Write(ent zapcore.Entry, fields []zapcore.Field) error {
|
||||
if !c.filter(fields) {
|
||||
return nil
|
||||
}
|
||||
return c.Core.Write(ent, fields)
|
||||
return logger
|
||||
}
|
||||
|
||||
@@ -284,9 +284,6 @@ flagger:
|
||||
# Config are the overrides for the feature flags which come directly from the config file.
|
||||
config:
|
||||
boolean:
|
||||
use_span_metrics: true
|
||||
interpolation_enabled: false
|
||||
kafka_span_eval: false
|
||||
string:
|
||||
float:
|
||||
integer:
|
||||
|
||||
@@ -176,7 +176,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.106.0
|
||||
image: signoz/signoz:v0.105.1
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
ports:
|
||||
|
||||
@@ -117,7 +117,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.106.0
|
||||
image: signoz/signoz:v0.105.1
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
ports:
|
||||
|
||||
@@ -179,7 +179,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.106.0}
|
||||
image: signoz/signoz:${VERSION:-v0.105.1}
|
||||
container_name: signoz
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
|
||||
@@ -111,7 +111,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.106.0}
|
||||
image: signoz/signoz:${VERSION:-v0.105.1}
|
||||
container_name: signoz
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
|
||||
@@ -2067,431 +2067,6 @@ paths:
|
||||
summary: Get features
|
||||
tags:
|
||||
- features
|
||||
/api/v2/metric/alerts:
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint returns associated alerts for a specified metric
|
||||
operationId: GetMetricAlerts
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/MetricsexplorertypesMetricAlertsResponse'
|
||||
status:
|
||||
type: string
|
||||
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 metric alerts
|
||||
tags:
|
||||
- metrics
|
||||
/api/v2/metric/dashboards:
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint returns associated dashboards for a specified metric
|
||||
operationId: GetMetricDashboards
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/MetricsexplorertypesMetricDashboardsResponse'
|
||||
status:
|
||||
type: string
|
||||
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 metric dashboards
|
||||
tags:
|
||||
- metrics
|
||||
/api/v2/metric/highlights:
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint returns highlights like number of datapoints, totaltimeseries,
|
||||
active time series, last received time for a specified metric
|
||||
operationId: GetMetricHighlights
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/MetricsexplorertypesMetricHighlightsResponse'
|
||||
status:
|
||||
type: string
|
||||
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 metric highlights
|
||||
tags:
|
||||
- metrics
|
||||
/api/v2/metrics/{metric_name}/metadata:
|
||||
post:
|
||||
deprecated: false
|
||||
description: This endpoint helps to update metadata information like metric
|
||||
description, unit, type, temporality, monotonicity for a specified metric
|
||||
operationId: UpdateMetricMetadata
|
||||
parameters:
|
||||
- in: path
|
||||
name: metric_name
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MetricsexplorertypesUpdateMetricMetadataRequest'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
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:
|
||||
- EDITOR
|
||||
- tokenizer:
|
||||
- EDITOR
|
||||
summary: Update metric metadata
|
||||
tags:
|
||||
- metrics
|
||||
/api/v2/metrics/attributes:
|
||||
post:
|
||||
deprecated: false
|
||||
description: This endpoint returns attribute keys and their unique values for
|
||||
a specified metric
|
||||
operationId: GetMetricAttributes
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MetricsexplorertypesMetricAttributesRequest'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/MetricsexplorertypesMetricAttributesResponse'
|
||||
status:
|
||||
type: string
|
||||
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 metric attributes
|
||||
tags:
|
||||
- metrics
|
||||
/api/v2/metrics/metadata:
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint returns metadata information like metric description,
|
||||
unit, type, temporality, monotonicity for a specified metric
|
||||
operationId: GetMetricMetadata
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/MetricsexplorertypesMetricMetadata'
|
||||
status:
|
||||
type: string
|
||||
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
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: Get metric metadata
|
||||
tags:
|
||||
- metrics
|
||||
/api/v2/metrics/stats:
|
||||
post:
|
||||
deprecated: false
|
||||
description: This endpoint provides list of metrics with their number of samples
|
||||
and timeseries for the given time range
|
||||
operationId: GetMetricsStats
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MetricsexplorertypesStatsRequest'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/MetricsexplorertypesStatsResponse'
|
||||
status:
|
||||
type: string
|
||||
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 metrics statistics
|
||||
tags:
|
||||
- metrics
|
||||
/api/v2/metrics/treemap:
|
||||
post:
|
||||
deprecated: false
|
||||
description: This endpoint returns a treemap visualization showing the proportional
|
||||
distribution of metrics by sample count or time series count
|
||||
operationId: GetMetricsTreemap
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MetricsexplorertypesTreemapRequest'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/MetricsexplorertypesTreemapResponse'
|
||||
status:
|
||||
type: string
|
||||
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 metrics treemap
|
||||
tags:
|
||||
- metrics
|
||||
/api/v2/orgs/me:
|
||||
get:
|
||||
deprecated: false
|
||||
@@ -3011,202 +2586,6 @@ components:
|
||||
nullable: true
|
||||
type: object
|
||||
type: object
|
||||
MetricsexplorertypesMetricAlert:
|
||||
properties:
|
||||
alertId:
|
||||
type: string
|
||||
alertName:
|
||||
type: string
|
||||
type: object
|
||||
MetricsexplorertypesMetricAlertsResponse:
|
||||
properties:
|
||||
alerts:
|
||||
items:
|
||||
$ref: '#/components/schemas/MetricsexplorertypesMetricAlert'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
MetricsexplorertypesMetricAttribute:
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
valueCount:
|
||||
minimum: 0
|
||||
type: integer
|
||||
values:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
MetricsexplorertypesMetricAttributesRequest:
|
||||
properties:
|
||||
end:
|
||||
nullable: true
|
||||
type: integer
|
||||
metricName:
|
||||
type: string
|
||||
start:
|
||||
nullable: true
|
||||
type: integer
|
||||
type: object
|
||||
MetricsexplorertypesMetricAttributesResponse:
|
||||
properties:
|
||||
attributes:
|
||||
items:
|
||||
$ref: '#/components/schemas/MetricsexplorertypesMetricAttribute'
|
||||
nullable: true
|
||||
type: array
|
||||
totalKeys:
|
||||
format: int64
|
||||
type: integer
|
||||
type: object
|
||||
MetricsexplorertypesMetricDashboard:
|
||||
properties:
|
||||
dashboardId:
|
||||
type: string
|
||||
dashboardName:
|
||||
type: string
|
||||
widgetId:
|
||||
type: string
|
||||
widgetName:
|
||||
type: string
|
||||
type: object
|
||||
MetricsexplorertypesMetricDashboardsResponse:
|
||||
properties:
|
||||
dashboards:
|
||||
items:
|
||||
$ref: '#/components/schemas/MetricsexplorertypesMetricDashboard'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
MetricsexplorertypesMetricHighlightsResponse:
|
||||
properties:
|
||||
activeTimeSeries:
|
||||
minimum: 0
|
||||
type: integer
|
||||
dataPoints:
|
||||
minimum: 0
|
||||
type: integer
|
||||
lastReceived:
|
||||
minimum: 0
|
||||
type: integer
|
||||
totalTimeSeries:
|
||||
minimum: 0
|
||||
type: integer
|
||||
type: object
|
||||
MetricsexplorertypesMetricMetadata:
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
isMonotonic:
|
||||
type: boolean
|
||||
temporality:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
unit:
|
||||
type: string
|
||||
type: object
|
||||
MetricsexplorertypesStat:
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
metricName:
|
||||
type: string
|
||||
samples:
|
||||
minimum: 0
|
||||
type: integer
|
||||
timeseries:
|
||||
minimum: 0
|
||||
type: integer
|
||||
type:
|
||||
type: string
|
||||
unit:
|
||||
type: string
|
||||
type: object
|
||||
MetricsexplorertypesStatsRequest:
|
||||
properties:
|
||||
end:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
limit:
|
||||
type: integer
|
||||
offset:
|
||||
type: integer
|
||||
orderBy:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5OrderBy'
|
||||
start:
|
||||
format: int64
|
||||
type: integer
|
||||
type: object
|
||||
MetricsexplorertypesStatsResponse:
|
||||
properties:
|
||||
metrics:
|
||||
items:
|
||||
$ref: '#/components/schemas/MetricsexplorertypesStat'
|
||||
nullable: true
|
||||
type: array
|
||||
total:
|
||||
minimum: 0
|
||||
type: integer
|
||||
type: object
|
||||
MetricsexplorertypesTreemapEntry:
|
||||
properties:
|
||||
metricName:
|
||||
type: string
|
||||
percentage:
|
||||
format: double
|
||||
type: number
|
||||
totalValue:
|
||||
minimum: 0
|
||||
type: integer
|
||||
type: object
|
||||
MetricsexplorertypesTreemapRequest:
|
||||
properties:
|
||||
end:
|
||||
format: int64
|
||||
type: integer
|
||||
filter:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Filter'
|
||||
limit:
|
||||
type: integer
|
||||
mode:
|
||||
type: string
|
||||
start:
|
||||
format: int64
|
||||
type: integer
|
||||
type: object
|
||||
MetricsexplorertypesTreemapResponse:
|
||||
properties:
|
||||
samples:
|
||||
items:
|
||||
$ref: '#/components/schemas/MetricsexplorertypesTreemapEntry'
|
||||
nullable: true
|
||||
type: array
|
||||
timeseries:
|
||||
items:
|
||||
$ref: '#/components/schemas/MetricsexplorertypesTreemapEntry'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
MetricsexplorertypesUpdateMetricMetadataRequest:
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
isMonotonic:
|
||||
type: boolean
|
||||
metricName:
|
||||
type: string
|
||||
temporality:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
unit:
|
||||
type: string
|
||||
type: object
|
||||
PreferencetypesPreference:
|
||||
properties:
|
||||
allowedScopes:
|
||||
@@ -3267,38 +2646,6 @@ components:
|
||||
rowsScanned:
|
||||
minimum: 0
|
||||
type: integer
|
||||
stepIntervals:
|
||||
additionalProperties:
|
||||
minimum: 0
|
||||
type: integer
|
||||
type: object
|
||||
type: object
|
||||
Querybuildertypesv5Filter:
|
||||
properties:
|
||||
expression:
|
||||
type: string
|
||||
type: object
|
||||
Querybuildertypesv5OrderBy:
|
||||
properties:
|
||||
direction:
|
||||
type: string
|
||||
key:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5OrderByKey'
|
||||
type: object
|
||||
Querybuildertypesv5OrderByKey:
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
fieldContext:
|
||||
type: string
|
||||
fieldDataType:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
signal:
|
||||
type: string
|
||||
unit:
|
||||
type: string
|
||||
type: object
|
||||
Querybuildertypesv5QueryData:
|
||||
properties:
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
# Flagger
|
||||
|
||||
Flagger is SigNoz's feature flagging system built on top of the [OpenFeature](https://openfeature.dev/) standard. It provides a unified interface for evaluating feature flags across the application, allowing features to be enabled, disabled, or configured dynamically without code changes.
|
||||
|
||||
> 💡 **Note**: OpenFeature is a CNCF project that provides a vendor-agnostic feature flagging API, making it easy to switch providers without changing application code.
|
||||
|
||||
## How does it work?
|
||||
|
||||
Flagger consists of three main components:
|
||||
|
||||
1. **Registry** (`pkg/flagger/registry.go`) - Contains all available feature flags with their metadata and default values
|
||||
2. **Flagger** (`pkg/flagger/flagger.go`) - The consumer-facing interface for evaluating feature flags
|
||||
3. **Providers** (`pkg/flagger/<provider>flagger/`) - Implementations that supply feature flag values (e.g., `configflagger` for config-based flags)
|
||||
|
||||
The evaluation flow works as follows:
|
||||
|
||||
1. The caller requests a feature flag value via the `Flagger` interface
|
||||
2. Flagger checks the registry to validate the flag exists and get its default value
|
||||
3. Each registered provider is queried for an override value
|
||||
4. If a provider returns a value different from the default, that value is returned
|
||||
5. Otherwise, the default value from the registry is returned
|
||||
|
||||
## How to add a new feature flag?
|
||||
|
||||
### 1. Register the flag in the registry
|
||||
|
||||
Add your feature flag definition in `pkg/flagger/registry.go`:
|
||||
|
||||
```go
|
||||
var (
|
||||
// Export the feature name for use in evaluations
|
||||
FeatureMyNewFeature = featuretypes.MustNewName("my_new_feature")
|
||||
)
|
||||
|
||||
func MustNewRegistry() featuretypes.Registry {
|
||||
registry, err := featuretypes.NewRegistry(
|
||||
// ...existing features...
|
||||
&featuretypes.Feature{
|
||||
Name: FeatureMyNewFeature,
|
||||
Kind: featuretypes.KindBoolean, // or KindString, KindFloat, KindInt, KindObject
|
||||
Stage: featuretypes.StageStable, // or StageAlpha, StageBeta
|
||||
Description: "Controls whether my new feature is enabled",
|
||||
DefaultVariant: featuretypes.MustNewName("disabled"),
|
||||
Variants: featuretypes.NewBooleanVariants(),
|
||||
},
|
||||
)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
> 💡 **Note**: Feature names must match the regex `^[a-z_]+$` (lowercase letters and underscores only).
|
||||
|
||||
### 2. Configure the feature flag value (optional)
|
||||
|
||||
To override the default value, add an entry in your configuration file:
|
||||
|
||||
```yaml
|
||||
flagger:
|
||||
config:
|
||||
boolean:
|
||||
my_new_feature: true
|
||||
```
|
||||
|
||||
Supported configuration types:
|
||||
|
||||
| Type | Config Key | Go Type |
|
||||
|------|------------|---------|
|
||||
| Boolean | `boolean` | `bool` |
|
||||
| String | `string` | `string` |
|
||||
| Float | `float` | `float64` |
|
||||
| Integer | `integer` | `int64` |
|
||||
| Object | `object` | `any` |
|
||||
|
||||
## How to evaluate a feature flag?
|
||||
|
||||
Use the `Flagger` interface to evaluate feature flags. The interface provides typed methods for each value type:
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
)
|
||||
|
||||
func DoSomething(ctx context.Context, flagger flagger.Flagger) error {
|
||||
// Create an evaluation context (typically with org ID)
|
||||
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
|
||||
|
||||
// Evaluate with error handling
|
||||
enabled, err := flagger.Boolean(ctx, flagger.FeatureMyNewFeature, evalCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if enabled {
|
||||
// Feature is enabled
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### Empty variants
|
||||
|
||||
For cases where you want to use a default value on error (and log the error), use the `*OrEmpty` methods:
|
||||
|
||||
```go
|
||||
func DoSomething(ctx context.Context, flagger flagger.Flagger) {
|
||||
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
|
||||
|
||||
// Returns false on error and logs the error
|
||||
if flagger.BooleanOrEmpty(ctx, flagger.FeatureMyNewFeature, evalCtx) {
|
||||
// Feature is enabled
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Available evaluation methods
|
||||
|
||||
| Method | Return Type | Empty Variant Default |
|
||||
|--------|-------------|---------------------|
|
||||
| `Boolean()` | `(bool, error)` | `false` |
|
||||
| `String()` | `(string, error)` | `""` |
|
||||
| `Float()` | `(float64, error)` | `0.0` |
|
||||
| `Int()` | `(int64, error)` | `0` |
|
||||
| `Object()` | `(any, error)` | `struct{}{}` |
|
||||
|
||||
## What should I remember?
|
||||
|
||||
- Always define feature flags in the registry (`pkg/flagger/registry.go`) before using them
|
||||
- Use descriptive feature names that clearly indicate what the flag controls
|
||||
- Prefer `*OrEmpty` methods for non-critical features to avoid error handling overhead
|
||||
- Export feature name variables (e.g., `FeatureMyNewFeature`) for type-safe usage across packages
|
||||
- Consider the feature's lifecycle stage (`Alpha`, `Beta`, `Stable`) when defining it
|
||||
- Providers are evaluated in order; the first non-default value wins
|
||||
@@ -1,301 +0,0 @@
|
||||
# Onboarding Configuration Guide
|
||||
|
||||
This guide explains how to add new data sources to the SigNoz onboarding flow. The onboarding configuration controls the "New Source" / "Get Started" experience in SigNoz Cloud.
|
||||
|
||||
## Configuration File Location
|
||||
|
||||
The configuration is located at:
|
||||
|
||||
```
|
||||
frontend/src/container/OnboardingV2Container/onboarding-configs/onboarding-config-with-links.json
|
||||
```
|
||||
|
||||
## JSON Structure Overview
|
||||
|
||||
The configuration file is a JSON array containing data source objects. Each object represents a selectable option in the onboarding flow.
|
||||
|
||||
## Data Source Object Keys
|
||||
|
||||
### Required Keys
|
||||
|
||||
| Key | Type | Description |
|
||||
|-----|------|-------------|
|
||||
| `dataSource` | `string` | Unique identifier for the data source (kebab-case, e.g., `"aws-ec2"`) |
|
||||
| `label` | `string` | Display name shown to users (e.g., `"AWS EC2"`) |
|
||||
| `tags` | `string[]` | Array of category tags for grouping (e.g., `["AWS"]`, `["database"]`) |
|
||||
| `module` | `string` | Destination module after onboarding completion |
|
||||
| `imgUrl` | `string` | Path to the logo/icon (e.g., `"/Logos/ec2.svg"`) |
|
||||
|
||||
### Optional Keys
|
||||
|
||||
| Key | Type | Description |
|
||||
|-----|------|-------------|
|
||||
| `link` | `string` | Docs link to redirect to (e.g., `"/docs/aws-monitoring/ec2/"`) |
|
||||
| `relatedSearchKeywords` | `string[]` | Array of keywords for search functionality |
|
||||
| `question` | `object` | Nested question object for multi-step flows |
|
||||
| `internalRedirect` | `boolean` | When `true`, navigates within the app instead of showing docs |
|
||||
|
||||
## Module Values
|
||||
|
||||
The `module` key determines where users are redirected after completing onboarding:
|
||||
|
||||
| Value | Destination |
|
||||
|-------|-------------|
|
||||
| `apm` | APM / Traces |
|
||||
| `logs` | Logs Explorer |
|
||||
| `metrics` | Metrics Explorer |
|
||||
| `dashboards` | Dashboards |
|
||||
| `infra-monitoring-hosts` | Infrastructure Monitoring - Hosts |
|
||||
| `infra-monitoring-k8s` | Infrastructure Monitoring - Kubernetes |
|
||||
| `messaging-queues-kafka` | Messaging Queues - Kafka |
|
||||
| `messaging-queues-celery` | Messaging Queues - Celery |
|
||||
| `integrations` | Integrations page |
|
||||
| `home` | Home page |
|
||||
| `api-monitoring` | API Monitoring |
|
||||
|
||||
## Question Object Structure
|
||||
|
||||
The `question` object enables multi-step selection flows:
|
||||
|
||||
```json
|
||||
{
|
||||
"question": {
|
||||
"desc": "What would you like to monitor?",
|
||||
"type": "select",
|
||||
"helpText": "Choose the telemetry type you want to collect.",
|
||||
"helpLink": "/docs/azure-monitoring/overview/",
|
||||
"helpLinkText": "Read the guide →",
|
||||
"options": [
|
||||
{
|
||||
"key": "logging",
|
||||
"label": "Logs",
|
||||
"imgUrl": "/Logos/azure-vm.svg",
|
||||
"link": "/docs/azure-monitoring/app-service/logging/"
|
||||
},
|
||||
{
|
||||
"key": "metrics",
|
||||
"label": "Metrics",
|
||||
"imgUrl": "/Logos/azure-vm.svg",
|
||||
"link": "/docs/azure-monitoring/app-service/metrics/"
|
||||
},
|
||||
{
|
||||
"key": "tracing",
|
||||
"label": "Traces",
|
||||
"imgUrl": "/Logos/azure-vm.svg",
|
||||
"link": "/docs/azure-monitoring/app-service/tracing/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Question Keys
|
||||
|
||||
| Key | Type | Description |
|
||||
|-----|------|-------------|
|
||||
| `desc` | `string` | Question text displayed to the user |
|
||||
| `type` | `string` | Currently only `"select"` is supported |
|
||||
| `helpText` | `string` | (Optional) Additional help text below the question |
|
||||
| `helpLink` | `string` | (Optional) Docs link for the help section |
|
||||
| `helpLinkText` | `string` | (Optional) Text for the help link (default: "Learn more →") |
|
||||
| `options` | `array` | Array of option objects |
|
||||
|
||||
## Option Object Structure
|
||||
|
||||
Options can be simple (direct link) or nested (with another question):
|
||||
|
||||
### Simple Option (Direct Link)
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "aws-ec2-logs",
|
||||
"label": "Logs",
|
||||
"imgUrl": "/Logos/ec2.svg",
|
||||
"link": "/docs/userguide/collect_logs_from_file/"
|
||||
}
|
||||
```
|
||||
|
||||
### Option with Internal Redirect
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "aws-ec2-metrics-one-click",
|
||||
"label": "One Click AWS",
|
||||
"imgUrl": "/Logos/ec2.svg",
|
||||
"link": "/integrations?integration=aws-integration&service=ec2",
|
||||
"internalRedirect": true
|
||||
}
|
||||
```
|
||||
|
||||
> **Important**: Set `internalRedirect: true` only for internal app routes (like `/integrations?...`). Docs links should NOT have this flag.
|
||||
|
||||
### Nested Option (Multi-step Flow)
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "aws-ec2-metrics",
|
||||
"label": "Metrics",
|
||||
"imgUrl": "/Logos/ec2.svg",
|
||||
"question": {
|
||||
"desc": "How would you like to set up monitoring?",
|
||||
"helpText": "Choose your setup method.",
|
||||
"options": [...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Simple Data Source (Direct Link)
|
||||
|
||||
```json
|
||||
{
|
||||
"dataSource": "aws-elb",
|
||||
"label": "AWS ELB",
|
||||
"tags": ["AWS"],
|
||||
"module": "logs",
|
||||
"relatedSearchKeywords": [
|
||||
"aws",
|
||||
"aws elb",
|
||||
"elb logs",
|
||||
"elastic load balancer"
|
||||
],
|
||||
"imgUrl": "/Logos/elb.svg",
|
||||
"link": "/docs/aws-monitoring/elb/"
|
||||
}
|
||||
```
|
||||
|
||||
### Data Source with Single Question Level
|
||||
|
||||
```json
|
||||
{
|
||||
"dataSource": "app-service",
|
||||
"label": "App Service",
|
||||
"imgUrl": "/Logos/azure-vm.svg",
|
||||
"tags": ["Azure"],
|
||||
"module": "apm",
|
||||
"relatedSearchKeywords": ["azure", "app service"],
|
||||
"question": {
|
||||
"desc": "What telemetry data do you want to visualise?",
|
||||
"type": "select",
|
||||
"options": [
|
||||
{
|
||||
"key": "logging",
|
||||
"label": "Logs",
|
||||
"imgUrl": "/Logos/azure-vm.svg",
|
||||
"link": "/docs/azure-monitoring/app-service/logging/"
|
||||
},
|
||||
{
|
||||
"key": "metrics",
|
||||
"label": "Metrics",
|
||||
"imgUrl": "/Logos/azure-vm.svg",
|
||||
"link": "/docs/azure-monitoring/app-service/metrics/"
|
||||
},
|
||||
{
|
||||
"key": "tracing",
|
||||
"label": "Traces",
|
||||
"imgUrl": "/Logos/azure-vm.svg",
|
||||
"link": "/docs/azure-monitoring/app-service/tracing/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Data Source with Nested Questions (2-3 Levels)
|
||||
|
||||
```json
|
||||
{
|
||||
"dataSource": "aws-ec2",
|
||||
"label": "AWS EC2",
|
||||
"tags": ["AWS"],
|
||||
"module": "logs",
|
||||
"relatedSearchKeywords": ["aws", "aws ec2", "ec2 logs", "ec2 metrics"],
|
||||
"imgUrl": "/Logos/ec2.svg",
|
||||
"question": {
|
||||
"desc": "What would you like to monitor for AWS EC2?",
|
||||
"type": "select",
|
||||
"helpText": "Choose the type of telemetry data you want to collect.",
|
||||
"options": [
|
||||
{
|
||||
"key": "aws-ec2-logs",
|
||||
"label": "Logs",
|
||||
"imgUrl": "/Logos/ec2.svg",
|
||||
"link": "/docs/userguide/collect_logs_from_file/"
|
||||
},
|
||||
{
|
||||
"key": "aws-ec2-metrics",
|
||||
"label": "Metrics",
|
||||
"imgUrl": "/Logos/ec2.svg",
|
||||
"question": {
|
||||
"desc": "How would you like to set up EC2 Metrics monitoring?",
|
||||
"helpText": "One Click uses AWS CloudWatch integration. Manual setup uses OpenTelemetry.",
|
||||
"helpLink": "/docs/aws-monitoring/one-click-vs-manual/",
|
||||
"helpLinkText": "Read the comparison guide →",
|
||||
"options": [
|
||||
{
|
||||
"key": "aws-ec2-metrics-one-click",
|
||||
"label": "One Click AWS",
|
||||
"imgUrl": "/Logos/ec2.svg",
|
||||
"link": "/integrations?integration=aws-integration&service=ec2",
|
||||
"internalRedirect": true
|
||||
},
|
||||
{
|
||||
"key": "aws-ec2-metrics-manual",
|
||||
"label": "Manual Setup",
|
||||
"imgUrl": "/Logos/ec2.svg",
|
||||
"link": "/docs/tutorial/opentelemetry-binary-usage-in-virtual-machine/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Tags
|
||||
|
||||
- Use existing tags when possible: `AWS`, `Azure`, `GCP`, `database`, `logs`, `apm/traces`, `infrastructure monitoring`, `LLM Monitoring`
|
||||
- Tags are used for grouping in the sidebar
|
||||
- Every data source must have at least one tag
|
||||
|
||||
### 2. Search Keywords
|
||||
|
||||
- Include variations of the name (e.g., `"aws ec2"`, `"ec2"`, `"ec2 logs"`)
|
||||
- Include common misspellings or alternative names
|
||||
- Keep keywords lowercase and alphabetically sorted
|
||||
|
||||
### 3. Logos
|
||||
|
||||
- Place logo files in `public/Logos/`
|
||||
- Use SVG format
|
||||
- Reference as `"/Logos/your-logo.svg"`
|
||||
|
||||
### 4. Links
|
||||
|
||||
- Docs links should start with `/docs/` (will be prefixed with DOCS_BASE_URL)
|
||||
- Internal app links should start with `/integrations`, `/services`, etc.
|
||||
- Only use `internalRedirect: true` for internal app routes
|
||||
|
||||
### 5. Keys
|
||||
|
||||
- Use kebab-case for `dataSource` and option `key` values
|
||||
- Make keys descriptive and unique
|
||||
- Follow the pattern: `{service}-{subtype}-{action}` (e.g., `aws-ec2-metrics-one-click`)
|
||||
|
||||
## Adding a New Data Source
|
||||
|
||||
1. Add your data source object to the JSON array
|
||||
2. Ensure the logo exists in `public/Logos/`
|
||||
3. Test the flow locally with `yarn dev`
|
||||
4. Validation:
|
||||
- Navigate to the [onboarding page](http://localhost:3301/get-started-with-signoz-cloud) on your local machine
|
||||
- Data source appears in the list
|
||||
- Search keywords work correctly
|
||||
- All links redirect to the correct pages
|
||||
- Questions display correct help text and links
|
||||
- Tags are used for grouping in the UI sidebar
|
||||
- Clicking on Configure redirects to the correct page
|
||||
@@ -9,10 +9,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/constants"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
pkgError "github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/licensetypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"go.uber.org/zap"
|
||||
@@ -26,7 +25,11 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(w, pkgError.Newf(pkgError.TypeInvalidInput, pkgError.CodeInvalidInput, "orgId is invalid"))
|
||||
return
|
||||
}
|
||||
|
||||
featureSet, err := ah.Signoz.Licensing.GetFeatureFlags(r.Context(), orgID)
|
||||
if err != nil {
|
||||
@@ -56,16 +59,13 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
|
||||
useSpanMetrics := ah.Signoz.Flagger.BooleanOrEmpty(ctx, flagger.FeatureUseSpanMetrics, evalCtx)
|
||||
|
||||
featureSet = append(featureSet, &licensetypes.Feature{
|
||||
Name: valuer.NewString(flagger.FeatureUseSpanMetrics.String()),
|
||||
Active: useSpanMetrics,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
})
|
||||
if constants.IsPreferSpanMetrics {
|
||||
for idx, feature := range featureSet {
|
||||
if feature.Name == licensetypes.UseSpanMetrics {
|
||||
featureSet[idx].Active = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if constants.IsDotMetricsEnabled {
|
||||
for idx, feature := range featureSet {
|
||||
|
||||
@@ -257,20 +257,18 @@ func (aH *APIHandler) queryRangeV5(rw http.ResponseWriter, req *http.Request) {
|
||||
results = append(results, item)
|
||||
}
|
||||
|
||||
// Build step intervals from the anomaly query
|
||||
stepIntervals := make(map[string]uint64)
|
||||
if anomalyQuery.StepInterval.Duration > 0 {
|
||||
stepIntervals[anomalyQuery.Name] = uint64(anomalyQuery.StepInterval.Duration.Seconds())
|
||||
}
|
||||
|
||||
finalResp := &qbtypes.QueryRangeResponse{
|
||||
Type: queryRangeRequest.RequestType,
|
||||
Data: qbtypes.QueryData{
|
||||
Data: struct {
|
||||
Results []any `json:"results"`
|
||||
}{
|
||||
Results: results,
|
||||
},
|
||||
Meta: qbtypes.ExecStats{
|
||||
StepIntervals: stepIntervals,
|
||||
},
|
||||
Meta: struct {
|
||||
RowsScanned uint64 `json:"rowsScanned"`
|
||||
BytesScanned uint64 `json:"bytesScanned"`
|
||||
DurationMS uint64 `json:"durationMs"`
|
||||
}{},
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, finalResp)
|
||||
|
||||
@@ -23,9 +23,14 @@ func GetOrDefaultEnv(key string, fallback string) string {
|
||||
const DotMetricsEnabled = "DOT_METRICS_ENABLED"
|
||||
|
||||
var IsDotMetricsEnabled = false
|
||||
var IsPreferSpanMetrics = false
|
||||
|
||||
func init() {
|
||||
if GetOrDefaultEnv(DotMetricsEnabled, "true") == "true" {
|
||||
IsDotMetricsEnabled = true
|
||||
}
|
||||
|
||||
if GetOrDefaultEnv("USE_SPAN_METRICS", "false") == "true" {
|
||||
IsPreferSpanMetrics = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +120,5 @@ module.exports = {
|
||||
usePrettierrc: true,
|
||||
},
|
||||
],
|
||||
'react/jsx-props-no-spreading': 'off',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<image width="100" height="100" href="data:image/webp;base64,UklGRqQEAABXRUJQVlA4IJgEAACQJQCdASrhAOEAPm02mUikIyKhJVZ4CIANiWVu4XPw+dx/zz8Y9kq6J+Kv5T8nN4H6MM2Hod6xffvuy+jnoX8wD9Zekj5gP2S/YDsAegB+ovWUeg9/GfPP/cn4dP2/9JnVbPMU2BWP/6ShYsB/INjyXNw88GLWEjqTk7r3B1Jyd0Q60mjXFTuiHUwvdoU9QiTOwXp8O6KdYSPcXtgV/YIjrUu+9aTZYQwhSBOiHuLpesMKhiEeAz91s6AFmAINNYJeBpyuf9KScX17NiZWEjlmJ+PsBWx4Be/O9Vng220j2g97IwDP4CHt7ScFK+ikuWNp4z1G60XIv5OXis+Sp/e/QkOjbE0qKut3VrL3hQfSVuuLp2XQBvxPw8eyiMbE05kQ6k5O6Ml23DSy92Ix+s9tw0svcgAA/Jba8JQ+0uNQuBewKWqirnyDRkjzUXXOeoNxj0dA3QpklBe8QAAMu8geavhwzfTJ2OUqOrwpg8PXz6sktEQLDnTlNclju/V9f4Ny3CgDYwBvljjNaFA8PR18BGAyzmVuHsa0hgskCkgbT6nZi/Vr9AXTc/UZtUS8Z3pKg4hMjWCXdMx0JCsu6v+8ghZmHf/y0VcGbDiugOy3WH36R3zSOIJh1NjhcNYlKfWilZD5ohEfCaTn/D4u3kgC/kLeLF06X5oe/zKySrWWEPQe965rTf4pcEGBZQGkauwtkLPJWVcHoNzJshb6gdVqs0hye7fI5nFTsbJaZmqz8kXeMSEV4zhApVaDyl3Q2P/ATfChzCP87kOBxeVrhSkNkMWS/e1j58dVzpyTubHOt04Z2NKpTlEjBganrkKIYvtuYWGoJWEmtMVtM9TuzNWa3jKX3kWc9zWmQHHawveaHnJn4HJXQwbTC4ZIlxa5el1b8GgqUXzOeF9LIWA3eb7i34/Q3GXZcbs5DLsukl03KclLo5hW5uf04JjGQR5NXHd8as68c3K9Kze3147QuIn1ytP7uE9us639xDuAiHDPr9JiVn62mv80Q2ErNLD+d6VYdhm6J2PCLY5/9mHTExTnIBzRtNoErP6qmMiCXmSM4sX7s14u3yhaE+dVjX1xtxUWtFjH0GWCN/1JNmjtElHq+FjqPWse7tIMvkKm7xFBgoM8yUJGxY+HviWNkOHipLPh7RJ3KehnEZTPIhR5GHlhlATLx+QUQ61+WwhffiT45KPRiK9v8Cd+1YShd6ZGqFSqsxt4FuMSvpMUB+Rd51pUUPuwXHzpXcyz6BXkWw0Mi3jbfH3kxet2Bsqzu0XWmJ2Oj097qP4ayirQzJewKHo8LS0cFG73Lvox2tmD7ZhuRRVmGjyz62sUMVDSzIUN2BHghed2IdUHVnAkof5DrdZIx+umeHp2XSityHdbTywo7r1+ve5+vPk8hhxhISBjbK+qikfPVnhJ8R/L7KUUqchh3AGKVmRGVgXh09MDaEiW/qqPYzP+MBiKfM5eTcHuGLymyFvT2em/o58zIcp7nKtDr90BsurnuOA0XaH3ZPDpSFAUN3HL9Zj7mpQtcFMfpCaC1VMv0l2UvY8xo/6qYAG6UEbsogAAAAA=" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg width="361" height="224" viewBox="0 0 361 224" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M67.3465 4.3208C62.5234 4.3208 58.0667 6.89389 55.6552 11.0708L1.80865 104.336C-0.602889 108.513 -0.602883 113.659 1.80866 117.836L55.6552 211.101C58.0667 215.277 62.5234 217.851 67.3465 217.851H175.039C179.863 217.851 184.319 215.277 186.731 211.101L240.577 117.836C242.989 113.659 242.989 108.513 240.577 104.336L186.731 11.0708C184.319 6.89388 179.863 4.3208 175.039 4.3208L67.3465 4.3208Z" fill="#D5E5FF"/>
|
||||
<path d="M287.008 2.29488C319.287 10.6012 315.609 28.9193 328.995 66.1793C337.583 90.084 379.104 119.725 341.16 177.344C329.837 194.54 293.425 214.941 267.079 220.6C209.035 232.875 174.834 205.018 144.494 159.09C121.73 124.349 129.773 74.335 164.299 49.7761C197.018 26.5653 244.576 -9.45146 287.008 2.29488Z" fill="#69A3FF"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 853 B |
@@ -1 +0,0 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>LiveKit</title><path d="M14 10h-4v4h4v-4zM18 6h-4v4.001h4v-4zM18 14h-4v4h4v-4zM22 2h-4v4h4V2zM22 18h-4v4h4v-4z" fill="#1FD5F9"></path><path d="M6 18V2H2v20h12v-4H6z" fill="#fff"></path></svg>
|
||||
|
Before Width: | Height: | Size: 316 B |
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="280" height="280">
|
||||
<path d="M0 0 C92.4 0 184.8 0 280 0 C280 92.4 280 184.8 280 280 C187.6 280 95.2 280 0 280 C0 187.6 0 95.2 0 0 Z " fill="#FEFEFE" transform="translate(0,0)"/>
|
||||
<path d="M0 0 C4.32048747 1.62223107 6.55486576 4.28030505 9.52734375 7.72265625 C10.09864014 8.36162842 10.66993652 9.00060059 11.25854492 9.65893555 C12.88271439 11.48112369 14.48833898 13.3180548 16.08984375 15.16015625 C16.78851563 15.9490625 17.4871875 16.73796875 18.20703125 17.55078125 C18.76648438 18.185 19.3259375 18.81921875 19.90234375 19.47265625 C20.40076782 20.02429947 20.89919189 20.57594269 21.41271973 21.14430237 C23.08984375 23.16015625 23.08984375 23.16015625 24.49304199 25.45811462 C27.47252811 29.81453108 29.71272364 31.88015293 34.97558594 32.95022583 C39.9205962 33.32069047 44.85650654 33.17298906 49.80859375 33.0390625 C51.63558812 33.02225785 53.46262328 33.00945545 55.28967285 33.00047302 C60.07802047 32.96625339 64.86387621 32.87797816 69.65124512 32.77813721 C74.54449761 32.68575224 79.43815637 32.64489784 84.33203125 32.59960938 C93.91905835 32.50334235 103.50424315 32.34991947 113.08984375 32.16015625 C113.38173584 31.5460791 113.67362793 30.93200195 113.97436523 30.29931641 C115.36028726 27.64152503 117.11916807 25.73686678 119.15234375 23.53515625 C119.95414063 22.65988281 120.7559375 21.78460938 121.58203125 20.8828125 C122.82339844 19.53509766 122.82339844 19.53509766 124.08984375 18.16015625 C125.6549797 16.43337512 127.21756353 14.70427667 128.77734375 12.97265625 C129.51984375 12.15539062 130.26234375 11.338125 131.02734375 10.49609375 C132.86324628 8.41679505 134.56732791 6.27970771 136.27734375 4.09765625 C138.30731891 1.73122112 139.11199786 1.15156588 142.15234375 -0.02734375 C146.07530165 0.22305782 147.37097322 1.35790253 150.08984375 4.16015625 C150.69111633 7.33660889 150.69111633 7.33660889 150.6574707 11.13183594 C150.6585659 12.17146736 150.6585659 12.17146736 150.65968323 13.23210144 C150.65620792 15.51621591 150.61735535 17.79827448 150.578125 20.08203125 C150.56879653 21.66814052 150.56168057 23.25426423 150.5566864 24.84039307 C150.53761889 29.00990759 150.48852138 33.17854766 150.4331665 37.34771729 C150.381968 41.60408702 150.35917115 45.86059842 150.33398438 50.1171875 C150.28040107 58.46528927 150.19246835 66.81251315 150.08984375 75.16015625 C158.00984375 75.16015625 165.92984375 75.16015625 174.08984375 75.16015625 C174.08984375 80.11015625 174.08984375 85.06015625 174.08984375 90.16015625 C161.54984375 90.16015625 149.00984375 90.16015625 136.08984375 90.16015625 C135.75984375 70.03015625 135.42984375 49.90015625 135.08984375 29.16015625 C126.87476058 37.9752561 126.87476058 37.9752561 119.08984375 47.16015625 C115.79274781 48.80870422 112.12209411 48.32562514 108.49682617 48.3371582 C107.60723709 48.3437294 106.71764801 48.3503006 105.80110168 48.35707092 C102.84917116 48.37682562 99.89729154 48.38847111 96.9453125 48.3984375 C95.43493125 48.40455079 95.43493125 48.40455079 93.89403725 48.41078758 C88.56502105 48.4316572 83.23603067 48.44595528 77.90698242 48.45532227 C72.39874211 48.46639094 66.89090531 48.50079372 61.38280773 48.54049397 C57.15089662 48.56662113 52.91908223 48.57501062 48.68709755 48.57860374 C46.6565001 48.58347949 44.62590725 48.59512145 42.59538841 48.61364174 C39.75361399 48.63795098 36.91289116 48.63708321 34.07104492 48.63012695 C33.23189545 48.64293701 32.39274597 48.65574707 31.52816772 48.66894531 C27.52364757 48.63320802 25.5843855 48.48069887 22.14155579 46.24919128 C20.24828704 44.32148181 18.40141019 42.40710858 16.65234375 40.34765625 C16.11738281 39.72246094 15.58242187 39.09726563 15.03125 38.453125 C14.39058594 37.69644531 13.74992187 36.93976563 13.08984375 36.16015625 C11.10984375 33.85015625 9.12984375 31.54015625 7.08984375 29.16015625 C6.75984375 49.29015625 6.42984375 69.42015625 6.08984375 90.16015625 C-6.45015625 90.16015625 -18.99015625 90.16015625 -31.91015625 90.16015625 C-31.91015625 85.21015625 -31.91015625 80.26015625 -31.91015625 75.16015625 C-23.99015625 75.16015625 -16.07015625 75.16015625 -7.91015625 75.16015625 C-7.93634033 73.03030273 -7.96252441 70.90044922 -7.98950195 68.70605469 C-8.07234699 61.66938098 -8.1275781 54.63268573 -8.16921711 47.59565353 C-8.19531224 43.32896888 -8.23070773 39.06278382 -8.28735352 34.79638672 C-8.34167786 30.67907045 -8.37157742 26.5622347 -8.38454247 22.44458771 C-8.39378174 20.87364384 -8.41182439 19.30272631 -8.43880653 17.731987 C-8.47510571 15.53135855 -8.48010172 13.33271556 -8.4777832 11.13183594 C-8.48888626 9.87941101 -8.49998932 8.62698608 -8.51142883 7.33660889 C-7.91015625 4.16015625 -7.91015625 4.16015625 -5.55656433 1.73443604 C-2.91015625 0.16015625 -2.91015625 0.16015625 0 0 Z " fill="#000000" transform="translate(68.91015625,79.83984375)"/>
|
||||
<path d="M0 0 C12.54 0 25.08 0 38 0 C38 4.95 38 9.9 38 15 C25.46 15 12.92 15 0 15 C0 10.05 0 5.1 0 0 Z " fill="#000000" transform="translate(205,185)"/>
|
||||
<path d="M0 0 C12.54 0 25.08 0 38 0 C38 4.95 38 9.9 38 15 C25.46 15 12.92 15 0 15 C0 10.05 0 5.1 0 0 Z " fill="#000000" transform="translate(37,185)"/>
|
||||
<path d="M0 0 C3.5386994 1.65139305 4.81070153 2.71605229 7 6 C7.34061873 9.0169088 7.57212107 11.09407815 7 14 C4.05423033 17.64345196 1.88105815 19.79099354 -2.8125 20.3125 C-6.7307645 19.92835642 -8.21293267 18.70743683 -11 16 C-12.74330022 12.51339956 -12.5706106 8.80407065 -12 5 C-8.64770496 0.02234979 -5.86333845 -0.83761978 0 0 Z " fill="#050505" transform="translate(109,150)"/>
|
||||
<path d="M0 0 C3.09459236 2.57882697 4.55423234 4.12558351 5.4765625 8.00390625 C5.75079474 11.80707441 5.1142302 13.83730851 2.9375 16.9375 C-0.57823396 19.40599406 -2.71383323 20 -7 20 C-10.57387602 18.41161066 -11.81089572 17.28365642 -14 14 C-14.67226891 7.05322129 -14.67226891 7.05322129 -12.125 3.0625 C-8.53966618 -0.41738282 -4.85970455 -0.95707375 0 0 Z " fill="#040404" transform="translate(178,150)"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.8 KiB |
@@ -1,25 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import {
|
||||
GlobalConfigData,
|
||||
GlobalConfigDataProps,
|
||||
} from 'types/api/globalConfig/types';
|
||||
|
||||
const getGlobalConfig = async (): Promise<
|
||||
SuccessResponseV2<GlobalConfigData>
|
||||
> => {
|
||||
try {
|
||||
const response = await axios.get<GlobalConfigDataProps>(`/global/config`);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default getGlobalConfig;
|
||||
@@ -0,0 +1,80 @@
|
||||
.field-variant-badges-container {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.field-badge {
|
||||
&.data-type {
|
||||
display: flex;
|
||||
height: 20px;
|
||||
padding: 4px 8px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border-radius: 20px;
|
||||
background: color-mix(in srgb, var(--bg-vanilla-100) 8%, transparent);
|
||||
white-space: nowrap;
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
|
||||
&.type-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 20px;
|
||||
padding: 0px 6px;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
border-radius: 50px;
|
||||
text-transform: capitalize;
|
||||
white-space: nowrap;
|
||||
|
||||
.dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
|
||||
&.attribute {
|
||||
background: color-mix(in srgb, var(--bg-sienna-400) 10%, transparent);
|
||||
color: var(--bg-sienna-400);
|
||||
|
||||
.dot {
|
||||
background-color: var(--bg-sienna-400);
|
||||
}
|
||||
|
||||
.text {
|
||||
color: var(--bg-sienna-400);
|
||||
}
|
||||
}
|
||||
|
||||
&.resource {
|
||||
background: color-mix(in srgb, var(--bg-aqua-400) 10%, transparent);
|
||||
color: var(--bg-aqua-400);
|
||||
|
||||
.dot {
|
||||
background-color: var(--bg-aqua-400);
|
||||
}
|
||||
|
||||
.text {
|
||||
color: var(--bg-aqua-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import './FieldVariantBadges.styles.scss';
|
||||
|
||||
import cx from 'classnames';
|
||||
|
||||
/**
|
||||
* Field contexts that should display badges
|
||||
*/
|
||||
export enum AllowedFieldContext {
|
||||
Attribute = 'attribute',
|
||||
Resource = 'resource',
|
||||
}
|
||||
|
||||
const ALLOWED_FIELD_CONTEXTS = new Set<string>([
|
||||
AllowedFieldContext.Attribute,
|
||||
AllowedFieldContext.Resource,
|
||||
]);
|
||||
|
||||
interface FieldVariantBadgesProps {
|
||||
fieldDataType?: string;
|
||||
fieldContext?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a fieldContext badge should be displayed
|
||||
* Only shows badges for contexts in ALLOWED_FIELD_CONTEXTS
|
||||
*/
|
||||
const shouldShowFieldContextBadge = (
|
||||
fieldContext: string | undefined | null,
|
||||
): boolean => {
|
||||
if (!fieldContext) {
|
||||
return false;
|
||||
}
|
||||
return ALLOWED_FIELD_CONTEXTS.has(fieldContext);
|
||||
};
|
||||
|
||||
function FieldVariantBadges({
|
||||
fieldDataType,
|
||||
fieldContext,
|
||||
}: FieldVariantBadgesProps): JSX.Element | null {
|
||||
// If neither value exists, don't render anything
|
||||
if (!fieldDataType && !fieldContext) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if fieldContext should be displayed
|
||||
const showFieldContext =
|
||||
fieldContext && shouldShowFieldContextBadge(fieldContext);
|
||||
|
||||
return (
|
||||
<span className="field-variant-badges-container">
|
||||
{fieldDataType && (
|
||||
<span className="field-badge data-type">{fieldDataType}</span>
|
||||
)}
|
||||
{showFieldContext && (
|
||||
<section className={cx('field-badge type-tag', fieldContext)}>
|
||||
<div className="dot" />
|
||||
<span className="text">{fieldContext}</span>
|
||||
</section>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
FieldVariantBadges.defaultProps = {
|
||||
fieldDataType: undefined,
|
||||
fieldContext: undefined,
|
||||
};
|
||||
|
||||
export default FieldVariantBadges;
|
||||
@@ -16,7 +16,7 @@
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
right: -2px;
|
||||
width: 240px;
|
||||
width: 280px;
|
||||
|
||||
.font-size-dropdown {
|
||||
display: flex;
|
||||
@@ -314,6 +314,23 @@
|
||||
background-color: var(--bg-ink-200);
|
||||
cursor: pointer;
|
||||
}
|
||||
.name-wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
.name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
@@ -402,12 +419,20 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
.name-wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: calc(100% - 26px);
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
.name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -6,8 +6,14 @@ import './LogsFormatOptionsMenu.styles.scss';
|
||||
import { Button, Input, InputNumber, Popover, Tooltip, Typography } from 'antd';
|
||||
import { DefaultOptionType } from 'antd/es/select';
|
||||
import cx from 'classnames';
|
||||
import FieldVariantBadges from 'components/FieldVariantBadges/FieldVariantBadges';
|
||||
import { LogViewMode } from 'container/LogsTable';
|
||||
import { FontSize, OptionsMenuConfig } from 'container/OptionsMenu/types';
|
||||
import {
|
||||
buildAttributeKey,
|
||||
getOptionLabelCounts,
|
||||
parseAttributeKey,
|
||||
} from 'container/OptionsMenu/utils';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import {
|
||||
Check,
|
||||
@@ -18,7 +24,7 @@ import {
|
||||
Sliders,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
interface LogsFormatOptionsMenuProps {
|
||||
items: any;
|
||||
@@ -208,6 +214,16 @@ function OptionsMenu({
|
||||
};
|
||||
}, [selectedValue]);
|
||||
|
||||
const optionsLabelCounts = useMemo(() => {
|
||||
const optionsKeys: { key: string }[] =
|
||||
addColumn?.options?.map((option) => ({ key: String(option.value) })) || [];
|
||||
const valuesKeys: { key: string }[] =
|
||||
addColumn?.value?.map((column) => ({ key: buildAttributeKey(column) })) ||
|
||||
[];
|
||||
|
||||
return getOptionLabelCounts([...optionsKeys, ...valuesKeys]);
|
||||
}, [addColumn?.options, addColumn?.value]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
@@ -301,33 +317,48 @@ function OptionsMenu({
|
||||
)}
|
||||
|
||||
<div className="column-format-new-options" ref={listRef}>
|
||||
{addColumn?.options?.map(({ label, value }, index) => (
|
||||
<div
|
||||
className={cx('column-name', value === selectedValue && 'selected')}
|
||||
key={value}
|
||||
onMouseEnter={(): void => {
|
||||
if (!initialMouseEnterRef.current) {
|
||||
setSelectedValue(value as string | null);
|
||||
}
|
||||
{addColumn?.options?.map(({ label, value }, index) => {
|
||||
const { name, fieldContext, fieldDataType } = parseAttributeKey(
|
||||
String(value),
|
||||
);
|
||||
const hasMultipleVariants = (optionsLabelCounts[name] || 0) > 1;
|
||||
return (
|
||||
<div
|
||||
className={cx('column-name', value === selectedValue && 'selected')}
|
||||
key={value}
|
||||
onMouseEnter={(): void => {
|
||||
if (!initialMouseEnterRef.current) {
|
||||
setSelectedValue(value as string | null);
|
||||
}
|
||||
|
||||
initialMouseEnterRef.current = true;
|
||||
}}
|
||||
onMouseMove={(): void => {
|
||||
// this is added to handle the mouse move explicit event and not the re-rendered on mouse enter event
|
||||
setSelectedValue(value as string | null);
|
||||
}}
|
||||
onClick={(eve): void => {
|
||||
eve.stopPropagation();
|
||||
handleColumnSelection(index, addColumn?.options || []);
|
||||
}}
|
||||
>
|
||||
<div className="name">
|
||||
<Tooltip placement="left" title={label}>
|
||||
{label}
|
||||
</Tooltip>
|
||||
initialMouseEnterRef.current = true;
|
||||
}}
|
||||
onMouseMove={(): void => {
|
||||
// this is added to handle the mouse move explicit event and not the re-rendered on mouse enter event
|
||||
setSelectedValue(value as string | null);
|
||||
}}
|
||||
onClick={(eve): void => {
|
||||
eve.stopPropagation();
|
||||
handleColumnSelection(index, addColumn?.options || []);
|
||||
}}
|
||||
>
|
||||
<div className="name-wrapper">
|
||||
<Tooltip placement="left" title={label}>
|
||||
<span className="name">{label}</span>
|
||||
</Tooltip>
|
||||
|
||||
{hasMultipleVariants && (
|
||||
<span className="field-variant-badges">
|
||||
<FieldVariantBadges
|
||||
fieldDataType={fieldDataType}
|
||||
fieldContext={fieldContext}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -416,22 +447,34 @@ function OptionsMenu({
|
||||
)}
|
||||
|
||||
<div className="column-format">
|
||||
{addColumn?.value?.map(({ name }) => (
|
||||
<div className="column-name" key={name}>
|
||||
<div className="name">
|
||||
<Tooltip placement="left" title={name}>
|
||||
{name}
|
||||
{addColumn?.value?.map((column) => {
|
||||
const uniqueKey = buildAttributeKey(column);
|
||||
const showBadge = (optionsLabelCounts[column.name] || 0) > 1;
|
||||
return (
|
||||
<div className="column-name" key={uniqueKey}>
|
||||
<Tooltip placement="left" title={column.name}>
|
||||
<div className="name-wrapper">
|
||||
<span className="name">{column.name}</span>
|
||||
{showBadge && (
|
||||
<span className="field-variant-badges">
|
||||
<FieldVariantBadges
|
||||
fieldDataType={column.fieldDataType}
|
||||
fieldContext={column.fieldContext}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{addColumn?.value?.length > 1 && (
|
||||
<X
|
||||
className="delete-btn"
|
||||
size={14}
|
||||
onClick={(): void => addColumn.onRemove(uniqueKey)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{addColumn?.value?.length > 1 && (
|
||||
<X
|
||||
className="delete-btn"
|
||||
size={14}
|
||||
onClick={(): void => addColumn.onRemove(name)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
{addColumn && addColumn?.value?.length === 0 && (
|
||||
<div className="column-name no-columns-selected">
|
||||
No columns selected
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { fireEvent, render, waitFor } from 'tests/test-utils';
|
||||
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||
|
||||
import LogsFormatOptionsMenu from '../LogsFormatOptionsMenu';
|
||||
|
||||
@@ -56,7 +57,16 @@ describe('LogsFormatOptionsMenu (unit)', () => {
|
||||
addColumn: {
|
||||
isFetching: false,
|
||||
value: [],
|
||||
options: [],
|
||||
options: [
|
||||
{
|
||||
label: 'http.status_code_attribute',
|
||||
value: 'http.status_code_attribute::dataType_string::context_attribute',
|
||||
},
|
||||
{
|
||||
label: 'http.status_code_attribute',
|
||||
value: 'http.status_code_attribute::dataType_number::context_resource',
|
||||
},
|
||||
],
|
||||
onFocus: jest.fn(),
|
||||
onBlur: jest.fn(),
|
||||
onSearch: jest.fn(),
|
||||
@@ -154,4 +164,22 @@ describe('LogsFormatOptionsMenu (unit)', () => {
|
||||
expect(fontSizeOnChange).toHaveBeenCalledWith(FontSize.MEDIUM);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders FieldVariantBadges for duplicate-name attribute options', async () => {
|
||||
setup();
|
||||
|
||||
// Open the "columns" section and then the add-new-column container by clicking plus
|
||||
// The popover is already open from setup()
|
||||
const plusInColumns = document.querySelector(
|
||||
'.selected-item-content-container .title svg',
|
||||
) as SVGElement;
|
||||
fireEvent.click(plusInColumns);
|
||||
|
||||
const items = screen.getAllByText('http.status_code_attribute', {
|
||||
exact: true,
|
||||
});
|
||||
expect(items).toHaveLength(2);
|
||||
expect(screen.getByText('context_attribute')).toBeInTheDocument();
|
||||
expect(screen.getByText('context_resource')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -627,7 +627,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
||||
) : (
|
||||
<Typography.Text
|
||||
className="value-string"
|
||||
ellipsis={{ tooltip: { placement: 'top' } }}
|
||||
ellipsis={{ tooltip: { placement: 'right' } }}
|
||||
>
|
||||
{String(value)}
|
||||
</Typography.Text>
|
||||
|
||||
@@ -68,8 +68,8 @@ export const metricQueryFunctionOptions: SelectOption<string, string>[] = [
|
||||
label: 'Time Shift',
|
||||
},
|
||||
{
|
||||
value: QueryFunctionsTypes.FILL_ZERO,
|
||||
label: 'Fill Zero',
|
||||
value: QueryFunctionsTypes.TIME_SHIFT,
|
||||
label: 'Time Shift',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -156,7 +156,4 @@ export const queryFunctionsTypesConfig: QueryFunctionConfigType = {
|
||||
showInput: true,
|
||||
inputType: 'text',
|
||||
},
|
||||
fillZero: {
|
||||
showInput: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3123,7 +3123,7 @@ export const getAllEndpointsWidgetData = (
|
||||
return widget;
|
||||
};
|
||||
|
||||
const keysToRemove = ['http.url', 'url.full', 'A', 'B', 'C', 'F1'];
|
||||
const keysToRemove = ['http.url', 'url.full', 'B', 'C', 'F1'];
|
||||
|
||||
export const getGroupByFiltersFromGroupByValues = (
|
||||
rowData: any,
|
||||
|
||||
@@ -996,7 +996,6 @@
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.ingestion-key-url-label {
|
||||
font-size: 13px;
|
||||
@@ -1058,26 +1057,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ingestion-url-error-tooltip {
|
||||
.ingestion-url-error-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ingestion-url-error-code {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--bg-amber-500);
|
||||
}
|
||||
|
||||
.ingestion-url-error-message {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.ingestion-setup-details-links {
|
||||
background: rgba(113, 144, 249, 0.1);
|
||||
@@ -1087,16 +1066,4 @@
|
||||
color: var(--bg-robin-500);
|
||||
}
|
||||
}
|
||||
|
||||
.ingestion-url-error-tooltip {
|
||||
.ingestion-url-error-content {
|
||||
.ingestion-url-error-code {
|
||||
color: var(--bg-amber-500);
|
||||
}
|
||||
}
|
||||
|
||||
.ingestion-url-error-message {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,9 +40,10 @@ import { initialQueryMeterWithType } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { INITIAL_ALERT_THRESHOLD_STATE } from 'container/CreateAlertV2/context/constants';
|
||||
import dayjs from 'dayjs';
|
||||
import { useGetGlobalConfig } from 'hooks/globalConfig/useGetGlobalConfig';
|
||||
import { useGetDeploymentsData } from 'hooks/CustomDomain/useGetDeploymentsData';
|
||||
import { useGetAllIngestionsKeys } from 'hooks/IngestionKeys/useGetAllIngestionKeys';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { cloneDeep, isNil, isUndefined } from 'lodash-es';
|
||||
import {
|
||||
@@ -58,12 +59,11 @@ import {
|
||||
PlusIcon,
|
||||
Search,
|
||||
Trash2,
|
||||
TriangleAlert,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { ChangeEvent, useCallback, useEffect, useState } from 'react';
|
||||
import { ChangeEvent, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMutation } from 'react-query';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
@@ -176,6 +176,8 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
|
||||
const [totalIngestionKeys, setTotalIngestionKeys] = useState(0);
|
||||
|
||||
const { isEnterpriseSelfHostedUser } = useGetTenantLicense();
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
const [
|
||||
@@ -300,11 +302,11 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
};
|
||||
|
||||
const {
|
||||
data: globalConfig,
|
||||
isLoading: isLoadingGlobalConfig,
|
||||
isError: isErrorGlobalConfig,
|
||||
error: globalConfigError,
|
||||
} = useGetGlobalConfig();
|
||||
data: deploymentsData,
|
||||
isLoading: isLoadingDeploymentsData,
|
||||
isFetching: isFetchingDeploymentsData,
|
||||
isError: isErrorDeploymentsData,
|
||||
} = useGetDeploymentsData(!isEnterpriseSelfHostedUser);
|
||||
|
||||
const {
|
||||
mutate: createIngestionKey,
|
||||
@@ -446,15 +448,12 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
});
|
||||
};
|
||||
|
||||
const handleCopyKey = useCallback(
|
||||
(text: string): void => {
|
||||
handleCopyToClipboard(text);
|
||||
notifications.success({
|
||||
message: 'Copied to clipboard',
|
||||
});
|
||||
},
|
||||
[handleCopyToClipboard, notifications],
|
||||
);
|
||||
const handleCopyKey = (text: string): void => {
|
||||
handleCopyToClipboard(text);
|
||||
notifications.success({
|
||||
message: 'Copied to clipboard',
|
||||
});
|
||||
};
|
||||
|
||||
const gbToBytes = (gb: number): number => Math.round(gb * 1024 ** 3);
|
||||
|
||||
@@ -1392,19 +1391,6 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
});
|
||||
};
|
||||
|
||||
const handleCopyIngestionURL = useCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement>): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
const ingestionURL = globalConfig?.data?.ingestion_url;
|
||||
if (ingestionURL) {
|
||||
handleCopyKey(ingestionURL);
|
||||
}
|
||||
},
|
||||
[globalConfig, handleCopyKey],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="ingestion-key-container">
|
||||
<div className="ingestion-key-content">
|
||||
@@ -1423,44 +1409,46 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
</Typography.Text>
|
||||
</header>
|
||||
|
||||
{!isLoadingGlobalConfig && (
|
||||
<div className="ingestion-setup-details-links">
|
||||
<div className="ingestion-key-url-container">
|
||||
<div className="ingestion-key-url-label">Ingestion URL</div>
|
||||
|
||||
{!isErrorGlobalConfig && (
|
||||
{!isErrorDeploymentsData &&
|
||||
!isLoadingDeploymentsData &&
|
||||
!isFetchingDeploymentsData &&
|
||||
deploymentsData && (
|
||||
<div className="ingestion-setup-details-links">
|
||||
<div className="ingestion-key-url-container">
|
||||
<div className="ingestion-key-url-label">Ingestion URL</div>
|
||||
<div
|
||||
className="ingestion-key-url-value"
|
||||
onClick={handleCopyIngestionURL}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleCopyKey(
|
||||
`ingest.${deploymentsData?.data.data.cluster.region.dns}`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
{globalConfig?.data.ingestion_url}
|
||||
ingest.{deploymentsData?.data.data.cluster.region.dns}
|
||||
<Copy className="copy-key-btn" size={12} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isErrorGlobalConfig && (
|
||||
<Tooltip
|
||||
rootClassName="ingestion-url-error-tooltip"
|
||||
arrow={false}
|
||||
title={
|
||||
<div className="ingestion-url-error-content">
|
||||
<Typography.Text className="ingestion-url-error-code">
|
||||
{globalConfigError?.getErrorCode()}
|
||||
</Typography.Text>
|
||||
|
||||
<Typography.Text className="ingestion-url-error-message">
|
||||
{globalConfigError?.getErrorMessage()}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
}
|
||||
placement="topLeft"
|
||||
<div className="ingestion-data-region-container">
|
||||
<div className="ingestion-data-region-label">Region</div>
|
||||
<div
|
||||
className="ingestion-data-region-value"
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleCopyKey(deploymentsData?.data.data.cluster.region.name || '');
|
||||
}}
|
||||
>
|
||||
<Button type="text" icon={<TriangleAlert size={14} />} />
|
||||
</Tooltip>
|
||||
)}
|
||||
<Typography.Text className="ingestion-data-region-value-text">
|
||||
{deploymentsData?.data.data.cluster.region.name}
|
||||
</Typography.Text>
|
||||
<Copy className="copy-key-btn" size={12} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
<div className="ingestion-keys-search-add-new">
|
||||
<Input
|
||||
|
||||
@@ -219,7 +219,7 @@ export default function TableViewActions(
|
||||
/>
|
||||
{!isListViewPanel && !RESTRICTED_SELECTED_FIELDS.includes(fieldFilterKey) && (
|
||||
<span className="action-btn">
|
||||
<Tooltip title="Filter for value" mouseLeaveDelay={0}>
|
||||
<Tooltip title="Filter for value">
|
||||
<Button
|
||||
className="filter-btn periscope-btn"
|
||||
icon={
|
||||
@@ -238,7 +238,7 @@ export default function TableViewActions(
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="Filter out value" mouseLeaveDelay={0}>
|
||||
<Tooltip title="Filter out value">
|
||||
<Button
|
||||
className="filter-btn periscope-btn"
|
||||
icon={
|
||||
@@ -299,7 +299,7 @@ export default function TableViewActions(
|
||||
</CopyClipboardHOC>
|
||||
{!isListViewPanel && !RESTRICTED_SELECTED_FIELDS.includes(fieldFilterKey) && (
|
||||
<span className="action-btn">
|
||||
<Tooltip title="Filter for value" mouseLeaveDelay={0}>
|
||||
<Tooltip title="Filter for value">
|
||||
<Button
|
||||
className="filter-btn periscope-btn"
|
||||
icon={
|
||||
@@ -318,7 +318,7 @@ export default function TableViewActions(
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="Filter out value" mouseLeaveDelay={0}>
|
||||
<Tooltip title="Filter out value">
|
||||
<Button
|
||||
className="filter-btn periscope-btn"
|
||||
icon={
|
||||
|
||||
@@ -20,7 +20,6 @@ import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { useGetPublicDashboardMeta } from 'hooks/dashboard/useGetPublicDashboardMeta';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
@@ -101,10 +100,6 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
handleDashboardLockToggle,
|
||||
} = useDashboard();
|
||||
|
||||
const { isCloudUser, isEnterpriseSelfHostedUser } = useGetTenantLicense();
|
||||
|
||||
const isPublicDashboardEnabled = isCloudUser || isEnterpriseSelfHostedUser;
|
||||
|
||||
const selectedData = selectedDashboard
|
||||
? {
|
||||
...selectedDashboard.data,
|
||||
@@ -308,14 +303,12 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
|
||||
const {
|
||||
data: publicDashboardResponse,
|
||||
// refetch: refetchPublicDashboardData,
|
||||
isLoading: isLoadingPublicDashboardData,
|
||||
isFetching: isFetchingPublicDashboardData,
|
||||
error: errorPublicDashboardData,
|
||||
isError: isErrorPublicDashboardData,
|
||||
} = useGetPublicDashboardMeta(
|
||||
selectedDashboard?.id || '',
|
||||
!!selectedDashboard?.id && isPublicDashboardEnabled,
|
||||
);
|
||||
} = useGetPublicDashboardMeta(selectedDashboard?.id || '');
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoadingPublicDashboardData && !isFetchingPublicDashboardData) {
|
||||
|
||||
@@ -35,11 +35,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.disabled-btn {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-ink-bar {
|
||||
|
||||
@@ -9,7 +9,6 @@ import { rest, server } from 'mocks-server/server';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
import PublicDashboardSetting from '../index';
|
||||
|
||||
@@ -108,11 +107,15 @@ describe('PublicDashboardSetting', () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByRole('checkbox', { name: /enable time range/i }),
|
||||
).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole('checkbox', { name: /enable time range/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(await screen.findByText(/default time range/i)).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/default time range/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText(/Last 30 minutes/i)).toBeInTheDocument();
|
||||
|
||||
@@ -123,7 +126,7 @@ describe('PublicDashboardSetting', () => {
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByRole('button', { name: /publish dashboard/i }),
|
||||
screen.getByRole('button', { name: /publish dashboard/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -148,9 +151,11 @@ describe('PublicDashboardSetting', () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByRole('checkbox', { name: /enable time range/i }),
|
||||
).toBeChecked();
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole('checkbox', { name: /enable time range/i }),
|
||||
).toBeChecked();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/default time range/i)).toBeInTheDocument();
|
||||
@@ -162,13 +167,17 @@ describe('PublicDashboardSetting', () => {
|
||||
expect(screen.getByText(/Public Dashboard URL/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByRole('button', { name: /update published dashboard/i }),
|
||||
).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole('button', { name: /update published dashboard/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByRole('button', { name: /unpublish dashboard/i }),
|
||||
).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole('button', { name: /unpublish dashboard/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -370,64 +379,4 @@ describe('PublicDashboardSetting', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Non-admin user permissions', () => {
|
||||
it('should disable "Publish dashboard" button for non-admin users', async () => {
|
||||
server.use(
|
||||
rest.get(
|
||||
`*/api/v1/dashboards/${MOCK_DASHBOARD_ID}/public`,
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(StatusCodes.NOT_FOUND),
|
||||
ctx.json(unpublishedPublicDashboardMeta),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
render(<PublicDashboardSetting />, {}, { role: USER_ROLES.VIEWER });
|
||||
|
||||
const publishButton = await screen.findByRole('button', {
|
||||
name: /publish dashboard/i,
|
||||
});
|
||||
expect(publishButton).toBeInTheDocument();
|
||||
expect(publishButton).toBeDisabled();
|
||||
expect(publishButton).toHaveAttribute('disabled');
|
||||
});
|
||||
|
||||
describe('Published dashboard buttons for non-admin users', () => {
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
rest.get(
|
||||
`*/api/v1/dashboards/${MOCK_DASHBOARD_ID}/public`,
|
||||
(_req, res, ctx) =>
|
||||
res(ctx.status(StatusCodes.OK), ctx.json(publishedPublicDashboardMeta)),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should disable "Unpublish dashboard" button for non-admin users', async () => {
|
||||
render(<PublicDashboardSetting />, {}, { role: USER_ROLES.VIEWER });
|
||||
|
||||
const unpublishButton = await screen.findByRole('button', {
|
||||
name: /unpublish dashboard/i,
|
||||
});
|
||||
expect(unpublishButton).toBeInTheDocument();
|
||||
expect(unpublishButton).toBeDisabled();
|
||||
expect(unpublishButton).toHaveAttribute('disabled');
|
||||
});
|
||||
|
||||
it('should disable "Update published dashboard" button for non-admin users', async () => {
|
||||
render(<PublicDashboardSetting />, {}, { role: USER_ROLES.VIEWER });
|
||||
|
||||
const updateButton = await screen.findByRole('button', {
|
||||
name: /update published dashboard/i,
|
||||
});
|
||||
expect(updateButton).toBeInTheDocument();
|
||||
expect(updateButton).toBeDisabled();
|
||||
expect(updateButton).toHaveAttribute('disabled');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,16 +8,12 @@ import createPublicDashboardAPI from 'api/dashboard/public/createPublicDashboard
|
||||
import revokePublicDashboardAccessAPI from 'api/dashboard/public/revokePublicDashboardAccess';
|
||||
import updatePublicDashboardAPI from 'api/dashboard/public/updatePublicDashboard';
|
||||
import { useGetPublicDashboardMeta } from 'hooks/dashboard/useGetPublicDashboardMeta';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { Copy, ExternalLink, Globe, Info, Loader2, Trash } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { PublicDashboardMetaProps } from 'types/api/dashboard/public/getMeta';
|
||||
import APIError from 'types/api/error';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
export const TIME_RANGE_PRESETS_OPTIONS = [
|
||||
{
|
||||
@@ -46,12 +42,6 @@ export const TIME_RANGE_PRESETS_OPTIONS = [
|
||||
},
|
||||
];
|
||||
|
||||
const showErrorNotification = (error: APIError): void => {
|
||||
toast.error(error.getErrorCode(), {
|
||||
description: error.getErrorMessage(),
|
||||
});
|
||||
};
|
||||
|
||||
function PublicDashboardSetting(): JSX.Element {
|
||||
const [publicDashboardData, setPublicDashboardData] = useState<
|
||||
PublicDashboardMetaProps | undefined
|
||||
@@ -62,14 +52,6 @@ function PublicDashboardSetting(): JSX.Element {
|
||||
|
||||
const { selectedDashboard } = useDashboard();
|
||||
|
||||
const { isCloudUser, isEnterpriseSelfHostedUser } = useGetTenantLicense();
|
||||
|
||||
const isPublicDashboardEnabled = isCloudUser || isEnterpriseSelfHostedUser;
|
||||
|
||||
const { user } = useAppContext();
|
||||
|
||||
const isAdmin = user?.role === USER_ROLES.ADMIN;
|
||||
|
||||
const handleDefaultTimeRange = useCallback((value: string): void => {
|
||||
setDefaultTimeRange(value);
|
||||
}, []);
|
||||
@@ -84,12 +66,9 @@ function PublicDashboardSetting(): JSX.Element {
|
||||
isFetching: isFetchingPublicDashboard,
|
||||
refetch: refetchPublicDashboard,
|
||||
error: errorPublicDashboard,
|
||||
} = useGetPublicDashboardMeta(
|
||||
selectedDashboard?.id || '',
|
||||
!!selectedDashboard?.id && isPublicDashboardEnabled,
|
||||
);
|
||||
} = useGetPublicDashboardMeta(selectedDashboard?.id || '');
|
||||
|
||||
const isPublicDashboard = !!publicDashboardData?.publicPath;
|
||||
const isPublicDashboardEnabled = !!publicDashboardData?.publicPath;
|
||||
|
||||
useEffect(() => {
|
||||
if (publicDashboardResponse?.data) {
|
||||
@@ -123,8 +102,8 @@ function PublicDashboardSetting(): JSX.Element {
|
||||
onSuccess: () => {
|
||||
toast.success('Public dashboard created successfully');
|
||||
},
|
||||
onError: (error: APIError) => {
|
||||
showErrorNotification(error);
|
||||
onError: () => {
|
||||
toast.error('Failed to create public dashboard');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -136,8 +115,8 @@ function PublicDashboardSetting(): JSX.Element {
|
||||
onSuccess: () => {
|
||||
toast.success('Public dashboard updated successfully');
|
||||
},
|
||||
onError: (error: APIError) => {
|
||||
showErrorNotification(error);
|
||||
onError: () => {
|
||||
toast.error('Failed to update public dashboard');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -149,8 +128,8 @@ function PublicDashboardSetting(): JSX.Element {
|
||||
onSuccess: () => {
|
||||
toast.success('Dashboard unpublished successfully');
|
||||
},
|
||||
onError: (error: APIError) => {
|
||||
showErrorNotification(error);
|
||||
onError: () => {
|
||||
toast.error('Failed to unpublish dashboard');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -231,7 +210,7 @@ function PublicDashboardSetting(): JSX.Element {
|
||||
level={5}
|
||||
className="public-dashboard-setting-content-title"
|
||||
>
|
||||
{isPublicDashboard
|
||||
{isPublicDashboardEnabled
|
||||
? 'This dashboard is publicly accessible. Anyone with the link can view it.'
|
||||
: 'This dashboard is private. Publish it to make it accessible to anyone with the link.'}
|
||||
</Typography.Title>
|
||||
@@ -261,7 +240,7 @@ function PublicDashboardSetting(): JSX.Element {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isPublicDashboard && (
|
||||
{isPublicDashboardEnabled && (
|
||||
<div className="public-dashboard-url">
|
||||
<div className="url-label-container">
|
||||
<Typography.Text className="url-label">
|
||||
@@ -302,11 +281,11 @@ function PublicDashboardSetting(): JSX.Element {
|
||||
</div>
|
||||
|
||||
<div className="public-dashboard-setting-actions">
|
||||
{!isPublicDashboard ? (
|
||||
{!isPublicDashboardEnabled ? (
|
||||
<Button
|
||||
type="primary"
|
||||
className="create-public-dashboard-btn periscope-btn primary"
|
||||
disabled={isLoading || !isAdmin}
|
||||
disabled={isLoading}
|
||||
onClick={handleCreatePublicDashboard}
|
||||
loading={
|
||||
isLoadingCreatePublicDashboard ||
|
||||
@@ -330,7 +309,7 @@ function PublicDashboardSetting(): JSX.Element {
|
||||
<Button
|
||||
type="default"
|
||||
className="periscope-btn secondary"
|
||||
disabled={isLoading || !isAdmin}
|
||||
disabled={isLoading}
|
||||
onClick={handleRevokePublicDashboardAccess}
|
||||
loading={isLoadingRevokePublicDashboardAccess}
|
||||
icon={<Trash size={14} />}
|
||||
@@ -341,7 +320,7 @@ function PublicDashboardSetting(): JSX.Element {
|
||||
<Button
|
||||
type="primary"
|
||||
className="create-public-dashboard-btn periscope-btn primary"
|
||||
disabled={isLoading || !isAdmin}
|
||||
disabled={isLoading}
|
||||
onClick={handleUpdatePublicDashboard}
|
||||
loading={isLoadingUpdatePublicDashboard}
|
||||
icon={<Globe size={14} />}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { useGetFieldKeys } from 'hooks/dynamicVariables/useGetFieldKeys';
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import './DashboardSettingsContent.styles.scss';
|
||||
|
||||
import { Button, Tabs, Tooltip } from 'antd';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { Button, Tabs } from 'antd';
|
||||
import { Braces, Globe, Table } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
import GeneralDashboardSettings from './General';
|
||||
import PublicDashboardSetting from './PublicDashboard';
|
||||
@@ -15,37 +12,6 @@ function DashboardSettingsContent({
|
||||
}: {
|
||||
variableViewModeRef: React.MutableRefObject<(() => void) | undefined>;
|
||||
}): JSX.Element {
|
||||
const { user } = useAppContext();
|
||||
const { isCloudUser, isEnterpriseSelfHostedUser } = useGetTenantLicense();
|
||||
|
||||
const enablePublicDashboard = isCloudUser || isEnterpriseSelfHostedUser;
|
||||
|
||||
const publicDashboardItem = {
|
||||
label: (
|
||||
<Tooltip
|
||||
title={
|
||||
user?.role !== USER_ROLES.ADMIN
|
||||
? 'Only admins can publish / manage public dashboards'
|
||||
: ''
|
||||
}
|
||||
placement="right"
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<Globe size={14} />}
|
||||
className={`public-dashboard-btn ${
|
||||
user?.role !== USER_ROLES.ADMIN ? 'disabled-btn' : ''
|
||||
}`}
|
||||
>
|
||||
Publish
|
||||
</Button>
|
||||
</Tooltip>
|
||||
),
|
||||
key: 'public-dashboard',
|
||||
children: <PublicDashboardSetting />,
|
||||
disabled: user?.role !== USER_ROLES.ADMIN,
|
||||
};
|
||||
|
||||
const items = [
|
||||
{
|
||||
label: (
|
||||
@@ -65,7 +31,19 @@ function DashboardSettingsContent({
|
||||
key: 'variables',
|
||||
children: <VariablesSetting variableViewModeRef={variableViewModeRef} />,
|
||||
},
|
||||
...(enablePublicDashboard ? [publicDashboardItem] : []),
|
||||
{
|
||||
label: (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<Globe size={14} />}
|
||||
className="public-dashboard-btn"
|
||||
>
|
||||
Publish
|
||||
</Button>
|
||||
),
|
||||
key: 'public-dashboard',
|
||||
children: <PublicDashboardSetting />,
|
||||
},
|
||||
];
|
||||
|
||||
return <Tabs items={items} animated className="settings-tabs" />;
|
||||
|
||||
@@ -51,15 +51,6 @@ interface Option {
|
||||
label: string;
|
||||
link?: string;
|
||||
entityID?: string;
|
||||
internalRedirect?: boolean;
|
||||
question?: {
|
||||
desc: string;
|
||||
options: Option[];
|
||||
entityID?: string;
|
||||
helpText?: string;
|
||||
helpLink?: string;
|
||||
helpLinkText?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface Entity {
|
||||
@@ -72,9 +63,6 @@ interface Entity {
|
||||
desc: string;
|
||||
options: Option[];
|
||||
entityID: string;
|
||||
helpText?: string;
|
||||
helpLink?: string;
|
||||
helpLinkText?: string;
|
||||
question?: {
|
||||
desc: string;
|
||||
options: Option[];
|
||||
@@ -119,32 +107,11 @@ const ONBOARDING_V3_ANALYTICS_EVENTS_MAP = {
|
||||
DATA_SOURCE_SEARCHED: 'Searched',
|
||||
};
|
||||
|
||||
const groupDataSourcesByTags = (
|
||||
dataSources: Entity[],
|
||||
): { [tag: string]: Entity[] } => {
|
||||
const groupedDataSources: { [tag: string]: Entity[] } = {};
|
||||
|
||||
dataSources.forEach((dataSource) => {
|
||||
dataSource.tags.forEach((tag) => {
|
||||
if (!groupedDataSources[tag]) {
|
||||
groupedDataSources[tag] = [];
|
||||
}
|
||||
groupedDataSources[tag].push(dataSource);
|
||||
});
|
||||
});
|
||||
|
||||
return groupedDataSources;
|
||||
};
|
||||
|
||||
const allGroupedDataSources = groupDataSourcesByTags(
|
||||
onboardingConfigWithLinks as Entity[],
|
||||
);
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function OnboardingAddDataSource(): JSX.Element {
|
||||
const [groupedDataSources, setGroupedDataSources] = useState<{
|
||||
[tag: string]: Entity[];
|
||||
}>(allGroupedDataSources);
|
||||
}>({});
|
||||
|
||||
const { org } = useAppContext();
|
||||
|
||||
@@ -240,33 +207,45 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
};
|
||||
|
||||
const handleSelectDataSource = (dataSource: Entity): void => {
|
||||
setSelectedDataSource(dataSource);
|
||||
setSelectedFramework(null);
|
||||
setSelectedEnvironment(null);
|
||||
|
||||
logEvent(
|
||||
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.DATA_SOURCE_SELECTED}`,
|
||||
{
|
||||
dataSource: dataSource.label,
|
||||
},
|
||||
);
|
||||
|
||||
if (dataSource.question) {
|
||||
setHasMoreQuestions(true);
|
||||
|
||||
setTimeout(() => {
|
||||
handleScrollToStep(question2Ref);
|
||||
}, 100);
|
||||
if (dataSource && dataSource.internalRedirect && dataSource.link) {
|
||||
logEvent(
|
||||
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.DATA_SOURCE_SELECTED}`,
|
||||
{
|
||||
dataSource: dataSource.label,
|
||||
},
|
||||
);
|
||||
history.push(dataSource.link);
|
||||
} else {
|
||||
setHasMoreQuestions(false);
|
||||
setSelectedDataSource(dataSource);
|
||||
setSelectedFramework(null);
|
||||
setSelectedEnvironment(null);
|
||||
|
||||
updateUrl(dataSource?.link || '', null);
|
||||
logEvent(
|
||||
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.DATA_SOURCE_SELECTED}`,
|
||||
{
|
||||
dataSource: dataSource.label,
|
||||
},
|
||||
);
|
||||
|
||||
setShowConfigureProduct(true);
|
||||
if (dataSource.question) {
|
||||
setHasMoreQuestions(true);
|
||||
|
||||
setTimeout(() => {
|
||||
handleScrollToStep(question2Ref);
|
||||
}, 100);
|
||||
} else {
|
||||
setHasMoreQuestions(false);
|
||||
|
||||
updateUrl(dataSource?.link || '', null);
|
||||
|
||||
setShowConfigureProduct(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectFramework = (option: any): void => {
|
||||
setSelectedFramework(option);
|
||||
|
||||
logEvent(
|
||||
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.FRAMEWORK_SELECTED}`,
|
||||
{
|
||||
@@ -275,8 +254,6 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
},
|
||||
);
|
||||
|
||||
setSelectedFramework(option);
|
||||
|
||||
if (option.question) {
|
||||
setHasMoreQuestions(true);
|
||||
|
||||
@@ -300,6 +277,9 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
selectedEnvironment: any,
|
||||
baseURL?: string,
|
||||
): void => {
|
||||
setSelectedEnvironment(selectedEnvironment);
|
||||
setHasMoreQuestions(false);
|
||||
|
||||
logEvent(
|
||||
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.ENVIRONMENT_SELECTED}`,
|
||||
{
|
||||
@@ -309,21 +289,45 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
},
|
||||
);
|
||||
|
||||
setSelectedEnvironment(selectedEnvironment);
|
||||
setHasMoreQuestions(false);
|
||||
|
||||
updateUrl(baseURL || docsUrl, selectedEnvironment?.key);
|
||||
|
||||
setShowConfigureProduct(true);
|
||||
};
|
||||
|
||||
const groupDataSourcesByTags = (
|
||||
dataSources: Entity[],
|
||||
): { [tag: string]: Entity[] } => {
|
||||
const groupedDataSources: { [tag: string]: Entity[] } = {};
|
||||
|
||||
dataSources.forEach((dataSource) => {
|
||||
dataSource.tags.forEach((tag) => {
|
||||
if (!groupedDataSources[tag]) {
|
||||
groupedDataSources[tag] = [];
|
||||
}
|
||||
groupedDataSources[tag].push(dataSource);
|
||||
});
|
||||
});
|
||||
|
||||
return groupedDataSources;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const groupedDataSources = groupDataSourcesByTags(
|
||||
onboardingConfigWithLinks as Entity[],
|
||||
);
|
||||
|
||||
setGroupedDataSources(groupedDataSources);
|
||||
}, []);
|
||||
|
||||
const debouncedUpdate = useDebouncedFn((query) => {
|
||||
setSearchQuery(query as string);
|
||||
|
||||
setDataSourceRequestSubmitted(false);
|
||||
|
||||
if (query === '') {
|
||||
setGroupedDataSources(allGroupedDataSources);
|
||||
setGroupedDataSources(
|
||||
groupDataSourcesByTags(onboardingConfigWithLinks as Entity[]),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -359,35 +363,31 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
},
|
||||
[debouncedUpdate],
|
||||
);
|
||||
|
||||
const handleFilterByCategory = (category: string): void => {
|
||||
setSelectedDataSource(null);
|
||||
setSelectedFramework(null);
|
||||
setSelectedEnvironment(null);
|
||||
|
||||
setSelectedCategory(category);
|
||||
|
||||
if (category === 'All') {
|
||||
setGroupedDataSources(allGroupedDataSources);
|
||||
setGroupedDataSources(
|
||||
groupDataSourcesByTags(onboardingConfigWithLinks as Entity[]),
|
||||
);
|
||||
|
||||
setSelectedCategory('All');
|
||||
return;
|
||||
}
|
||||
|
||||
if (allGroupedDataSources[category]) {
|
||||
setGroupedDataSources({
|
||||
[category]: allGroupedDataSources[category],
|
||||
});
|
||||
} else {
|
||||
// Fallback if somehow the category key doesn't strictly match or relies on partial match
|
||||
// This preserves the old behavior as a fallback, though sidebar clicks should be exact matches
|
||||
const filteredDataSources = onboardingConfigWithLinks.filter(
|
||||
(dataSource) =>
|
||||
dataSource.tags.includes(category) ||
|
||||
dataSource.tags.some((tag) => tag.toLowerCase().includes(category)),
|
||||
);
|
||||
setGroupedDataSources(
|
||||
groupDataSourcesByTags(filteredDataSources as Entity[]),
|
||||
);
|
||||
}
|
||||
const filteredDataSources = onboardingConfigWithLinks.filter(
|
||||
(dataSource) =>
|
||||
dataSource.tags.includes(category) ||
|
||||
dataSource.tags.some((tag) => tag.toLowerCase().includes(category)),
|
||||
);
|
||||
|
||||
setSelectedCategory(category);
|
||||
|
||||
setGroupedDataSources(
|
||||
groupDataSourcesByTags(filteredDataSources as Entity[]),
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -781,7 +781,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
{Object.keys(allGroupedDataSources).map((tag) => (
|
||||
{Object.keys(groupedDataSources).map((tag) => (
|
||||
<div
|
||||
key={tag}
|
||||
className="onboarding-data-source-category-item"
|
||||
@@ -806,7 +806,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
<div className="line-divider" />
|
||||
|
||||
<Typography.Text className="onboarding-filters-item-count">
|
||||
{allGroupedDataSources[tag].length}
|
||||
{groupedDataSources[tag].length}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
))}
|
||||
@@ -831,22 +831,6 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
>
|
||||
{selectedDataSource?.question?.desc}
|
||||
</Typography.Title>
|
||||
{selectedDataSource?.question?.helpText && (
|
||||
<Typography.Text className="question-help-text">
|
||||
{selectedDataSource?.question?.helpText}
|
||||
{selectedDataSource?.question?.helpLink && (
|
||||
<a
|
||||
href={`${DOCS_BASE_URL}${selectedDataSource?.question?.helpLink}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="question-help-link"
|
||||
>
|
||||
{selectedDataSource?.question?.helpLinkText ||
|
||||
'Learn more →'}
|
||||
</a>
|
||||
)}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="onboarding-data-source-options">
|
||||
@@ -901,22 +885,6 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
>
|
||||
{selectedFramework?.question?.desc}
|
||||
</Typography.Title>
|
||||
{selectedFramework?.question?.helpText && (
|
||||
<Typography.Text className="question-help-text">
|
||||
{selectedFramework?.question?.helpText}
|
||||
{selectedFramework?.question?.helpLink && (
|
||||
<a
|
||||
href={`${DOCS_BASE_URL}${selectedFramework?.question?.helpLink}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="question-help-link"
|
||||
>
|
||||
{selectedFramework?.question?.helpLinkText ||
|
||||
'Learn more →'}
|
||||
</a>
|
||||
)}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="onboarding-data-source-options">
|
||||
@@ -927,9 +895,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
selectedEnvironment?.label === option.label ? 'selected' : ''
|
||||
}`}
|
||||
type="primary"
|
||||
onClick={(): void =>
|
||||
handleSelectEnvironment(option, option.link)
|
||||
}
|
||||
onClick={(): void => handleSelectEnvironment(option)}
|
||||
>
|
||||
<img
|
||||
src={option.imgUrl || '/Logos/signoz-brand-logo-new.svg'}
|
||||
@@ -960,14 +926,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
},
|
||||
);
|
||||
|
||||
const currentEntity =
|
||||
selectedEnvironment || selectedFramework || selectedDataSource;
|
||||
|
||||
if (currentEntity?.internalRedirect && currentEntity?.link) {
|
||||
history.push(currentEntity.link);
|
||||
} else {
|
||||
handleUpdateCurrentStep(2);
|
||||
}
|
||||
handleUpdateCurrentStep(2);
|
||||
}}
|
||||
>
|
||||
Next: Configure your product
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
.ingestion-url-error-tooltip {
|
||||
.ingestion-url-error-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ingestion-url-error-code {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--bg-amber-500);
|
||||
}
|
||||
|
||||
.ingestion-url-error-message {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.ingestion-url-error-tooltip {
|
||||
.ingestion-url-error-content {
|
||||
.ingestion-url-error-code {
|
||||
color: var(--bg-amber-500);
|
||||
}
|
||||
}
|
||||
|
||||
.ingestion-url-error-message {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,18 @@
|
||||
import './IngestionDetails.styles.scss';
|
||||
|
||||
import { Button, Skeleton, Tooltip, Typography } from 'antd';
|
||||
import { Skeleton, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { AxiosError } from 'axios';
|
||||
import { DOCS_BASE_URL } from 'constants/app';
|
||||
import { useGetGlobalConfig } from 'hooks/globalConfig/useGetGlobalConfig';
|
||||
import { useGetDeploymentsData } from 'hooks/CustomDomain/useGetDeploymentsData';
|
||||
import { useGetAllIngestionsKeys } from 'hooks/IngestionKeys/useGetAllIngestionKeys';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { ArrowUpRight, Copy, Info, Key, TriangleAlert } from 'lucide-react';
|
||||
import {
|
||||
ArrowUpRight,
|
||||
Copy,
|
||||
Info,
|
||||
Key,
|
||||
MapPin,
|
||||
TriangleAlert,
|
||||
} from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { IngestionKeyProps } from 'types/api/ingestionKeys/types';
|
||||
@@ -55,11 +60,11 @@ export default function OnboardingIngestionDetails(): JSX.Element {
|
||||
});
|
||||
|
||||
const {
|
||||
data: globalConfig,
|
||||
isLoading: isLoadingGlobalConfig,
|
||||
isError: isErrorGlobalConfig,
|
||||
error: globalConfigError,
|
||||
} = useGetGlobalConfig();
|
||||
data: deploymentsData,
|
||||
isLoading: isLoadingDeploymentsData,
|
||||
isFetching: isFetchingDeploymentsData,
|
||||
isError: isDeploymentsDataError,
|
||||
} = useGetDeploymentsData(true);
|
||||
|
||||
const handleCopyKey = (text: string): void => {
|
||||
handleCopyToClipboard(text);
|
||||
@@ -109,7 +114,9 @@ export default function OnboardingIngestionDetails(): JSX.Element {
|
||||
</Typography.Text>
|
||||
|
||||
<div className="ingestion-key-details-section-key">
|
||||
{isIngestionKeysLoading || isLoadingGlobalConfig ? (
|
||||
{isIngestionKeysLoading ||
|
||||
isLoadingDeploymentsData ||
|
||||
isFetchingDeploymentsData ? (
|
||||
<div className="skeleton-container">
|
||||
<Skeleton.Input active className="skeleton-input" />
|
||||
<Skeleton.Input active className="skeleton-input" />
|
||||
@@ -118,58 +125,34 @@ export default function OnboardingIngestionDetails(): JSX.Element {
|
||||
</div>
|
||||
) : (
|
||||
<div className="ingestion-key-region-details-section">
|
||||
{!isLoadingGlobalConfig && (
|
||||
<div className="ingestion-region-container">
|
||||
<Typography.Text className="ingestion-region-label">
|
||||
Ingestion URL
|
||||
</Typography.Text>
|
||||
{!isDeploymentsDataError &&
|
||||
!isLoadingDeploymentsData &&
|
||||
!isFetchingDeploymentsData && (
|
||||
<div className="ingestion-region-container">
|
||||
<Typography.Text className="ingestion-region-label">
|
||||
<MapPin size={14} /> Region
|
||||
</Typography.Text>
|
||||
|
||||
{!isErrorGlobalConfig && (
|
||||
<Typography.Text className="ingestion-region-value-copy">
|
||||
{globalConfig?.data?.ingestion_url}
|
||||
{deploymentsData?.data?.data?.cluster.region.name}
|
||||
|
||||
<Copy
|
||||
size={14}
|
||||
className="copy-btn"
|
||||
onClick={(): void => {
|
||||
logEvent(
|
||||
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.INGESTION_URL_COPIED}`,
|
||||
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.REGION_COPIED}`,
|
||||
{},
|
||||
);
|
||||
|
||||
const ingestionURL = globalConfig?.data?.ingestion_url;
|
||||
|
||||
if (ingestionURL) {
|
||||
handleCopyKey(ingestionURL);
|
||||
}
|
||||
handleCopyKey(
|
||||
deploymentsData?.data?.data?.cluster.region.name || '',
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Typography.Text>
|
||||
)}
|
||||
|
||||
{isErrorGlobalConfig && (
|
||||
<Tooltip
|
||||
rootClassName="ingestion-url-error-tooltip"
|
||||
arrow={false}
|
||||
title={
|
||||
<div className="ingestion-url-error-content">
|
||||
<Typography.Text className="ingestion-url-error-code">
|
||||
{globalConfigError?.getErrorCode()}
|
||||
</Typography.Text>
|
||||
|
||||
<Typography.Text className="ingestion-url-error-message">
|
||||
{globalConfigError?.getErrorMessage()}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
}
|
||||
placement="topLeft"
|
||||
>
|
||||
<Button type="text" icon={<TriangleAlert size={14} />} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)}
|
||||
<div className="ingestion-key-container">
|
||||
<Typography.Text className="ingestion-key-label">
|
||||
<Key size={14} /> Ingestion Key
|
||||
|
||||
@@ -577,38 +577,6 @@
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.question-help-text {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
color: var(--text-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
|
||||
.lightMode & {
|
||||
color: var(--text-ink-300);
|
||||
}
|
||||
}
|
||||
|
||||
.question-help-link {
|
||||
margin-left: 4px;
|
||||
color: var(--bg-robin-400);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--bg-robin-500);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.lightMode & {
|
||||
color: var(--bg-robin-500);
|
||||
}
|
||||
}
|
||||
|
||||
.onboarding-question {
|
||||
color: var(--text-vanilla-100);
|
||||
font-family: Inter;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,22 +1,76 @@
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { Input, Spin, Typography } from 'antd';
|
||||
import { Input, Spin } from 'antd';
|
||||
import { BaseOptionType } from 'antd/es/select';
|
||||
import FieldVariantBadges from 'components/FieldVariantBadges/FieldVariantBadges';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { FieldTitle } from '../styles';
|
||||
import { OptionsMenuConfig } from '../types';
|
||||
import {
|
||||
buildAttributeKey,
|
||||
getOptionLabelCounts,
|
||||
parseAttributeKey,
|
||||
} from '../utils';
|
||||
import {
|
||||
AddColumnItem,
|
||||
AddColumnSelect,
|
||||
AddColumnWrapper,
|
||||
DeleteOutlinedIcon,
|
||||
Name,
|
||||
NameWrapper,
|
||||
OptionContent,
|
||||
SearchIconWrapper,
|
||||
} from './styles';
|
||||
|
||||
function OptionRenderer({
|
||||
option,
|
||||
optionsLabelCounts,
|
||||
}: {
|
||||
option: BaseOptionType;
|
||||
optionsLabelCounts: Record<string, number>;
|
||||
}): JSX.Element {
|
||||
const { label, data } = option;
|
||||
const key = data?.value as string;
|
||||
const { name, fieldContext, fieldDataType } = parseAttributeKey(key);
|
||||
const hasMultipleVariants = (optionsLabelCounts[name] || 0) > 1;
|
||||
return (
|
||||
<OptionContent>
|
||||
<span className="option-label">{label}</span>
|
||||
{hasMultipleVariants && (
|
||||
<FieldVariantBadges
|
||||
fieldDataType={fieldDataType}
|
||||
fieldContext={fieldContext}
|
||||
/>
|
||||
)}
|
||||
</OptionContent>
|
||||
);
|
||||
}
|
||||
|
||||
function AddColumnField({ config }: AddColumnFieldProps): JSX.Element | null {
|
||||
const { t } = useTranslation(['trace']);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const optionsLabelCounts = useMemo(() => {
|
||||
if (!config) return {};
|
||||
const optionsKeys: { key: string }[] =
|
||||
config?.options?.map((option) => ({ key: String(option.value) })) || [];
|
||||
const valuesKeys: { key: string }[] =
|
||||
config?.value?.map((column) => ({ key: buildAttributeKey(column) })) || [];
|
||||
|
||||
return getOptionLabelCounts([...optionsKeys, ...valuesKeys]);
|
||||
}, [config]);
|
||||
|
||||
const renderOption = useCallback(
|
||||
function renderOption(option: BaseOptionType): JSX.Element {
|
||||
return (
|
||||
<OptionRenderer option={option} optionsLabelCounts={optionsLabelCounts} />
|
||||
);
|
||||
},
|
||||
[optionsLabelCounts],
|
||||
);
|
||||
|
||||
if (!config) return null;
|
||||
|
||||
return (
|
||||
@@ -33,21 +87,32 @@ function AddColumnField({ config }: AddColumnFieldProps): JSX.Element | null {
|
||||
value={[]}
|
||||
onSelect={config.onSelect}
|
||||
onSearch={config.onSearch}
|
||||
onFocus={config.onFocus}
|
||||
onBlur={config.onBlur}
|
||||
notFoundContent={config.isFetching ? <Spin size="small" /> : null}
|
||||
optionRender={renderOption}
|
||||
/>
|
||||
<SearchIconWrapper $isDarkMode={isDarkMode}>
|
||||
<SearchOutlined />
|
||||
</SearchIconWrapper>
|
||||
</Input.Group>
|
||||
|
||||
{config.value?.map(({ name }) => (
|
||||
<AddColumnItem direction="horizontal" key={name}>
|
||||
<Typography>{name}</Typography>
|
||||
<DeleteOutlinedIcon onClick={(): void => config.onRemove(name)} />
|
||||
</AddColumnItem>
|
||||
))}
|
||||
{config.value?.map((column) => {
|
||||
const uniqueKey = buildAttributeKey(column);
|
||||
const showBadge = (optionsLabelCounts[column.name] || 0) > 1;
|
||||
return (
|
||||
<AddColumnItem key={uniqueKey}>
|
||||
<NameWrapper>
|
||||
<Name>{column.name}</Name>
|
||||
{showBadge && (
|
||||
<FieldVariantBadges
|
||||
fieldDataType={column.fieldDataType}
|
||||
fieldContext={column.fieldContext}
|
||||
/>
|
||||
)}
|
||||
</NameWrapper>
|
||||
<DeleteOutlinedIcon onClick={(): void => config.onRemove(uniqueKey)} />
|
||||
</AddColumnItem>
|
||||
);
|
||||
})}
|
||||
</AddColumnWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,8 +28,9 @@ export const AddColumnWrapper = styled(Space)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const AddColumnItem = styled(Space)`
|
||||
export const AddColumnItem = styled.div`
|
||||
width: 100%;
|
||||
min-width: 300px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
@@ -37,3 +38,35 @@ export const AddColumnItem = styled(Space)`
|
||||
export const DeleteOutlinedIcon = styled(DeleteOutlined)`
|
||||
color: red;
|
||||
`;
|
||||
|
||||
export const OptionContent = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
|
||||
.option-label {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
`;
|
||||
export const NameWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: calc(100% - 26px);
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
`;
|
||||
export const Name = styled.span`
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
@@ -52,7 +52,7 @@ export const defaultTraceSelectedColumns: TelemetryFieldKey[] = [
|
||||
name: 'duration_nano',
|
||||
signal: 'traces',
|
||||
fieldContext: 'span',
|
||||
fieldDataType: '',
|
||||
fieldDataType: 'number',
|
||||
},
|
||||
{
|
||||
name: 'http_method',
|
||||
|
||||
@@ -41,7 +41,18 @@ function OptionsMenu({
|
||||
|
||||
return (
|
||||
<OptionsContainer>
|
||||
<Popover placement="bottom" trigger="click" content={OptionsContent}>
|
||||
<Popover
|
||||
placement="bottom"
|
||||
trigger="click"
|
||||
content={OptionsContent}
|
||||
onOpenChange={(open: boolean): void => {
|
||||
if (!open) {
|
||||
config?.addColumn?.onBlur?.();
|
||||
} else {
|
||||
config?.addColumn?.onFocus?.();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Space align="center">
|
||||
{t('options_menu.options')}
|
||||
<SettingIcon />
|
||||
|
||||
@@ -31,12 +31,11 @@ export type OptionsMenuConfig = {
|
||||
};
|
||||
maxLines?: Pick<InputNumberProps, 'value' | 'onChange'>;
|
||||
fontSize?: FontSizeProps;
|
||||
addColumn?: Pick<
|
||||
SelectProps,
|
||||
'options' | 'onSelect' | 'onFocus' | 'onSearch' | 'onBlur'
|
||||
> & {
|
||||
addColumn?: Pick<SelectProps, 'options' | 'onSelect' | 'onSearch'> & {
|
||||
isFetching: boolean;
|
||||
value: TelemetryFieldKey[];
|
||||
onRemove: (key: string) => void;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
OptionsMenuConfig,
|
||||
OptionsQuery,
|
||||
} from './types';
|
||||
import { getOptionsFromKeys } from './utils';
|
||||
import { buildAttributeKey, getOptionsFromKeys } from './utils';
|
||||
|
||||
interface UseOptionsMenuProps {
|
||||
storageKey?: string;
|
||||
@@ -262,7 +262,7 @@ const useOptionsMenu = ({
|
||||
}, [dataSource, initialOptions, initialSelectedColumns]);
|
||||
|
||||
const selectedColumnKeys = useMemo(
|
||||
() => preferences?.columns?.map(({ name }) => name) || [],
|
||||
() => preferences?.columns?.map(buildAttributeKey) || [],
|
||||
[preferences?.columns],
|
||||
);
|
||||
|
||||
@@ -292,7 +292,7 @@ const useOptionsMenu = ({
|
||||
const column = [
|
||||
...searchedAttributeKeys,
|
||||
...(preferences?.columns || []),
|
||||
].find(({ name }) => name === key);
|
||||
].find((k) => buildAttributeKey(k) === key);
|
||||
|
||||
if (!column) return acc;
|
||||
return [...acc, column];
|
||||
@@ -321,7 +321,7 @@ const useOptionsMenu = ({
|
||||
const handleRemoveSelectedColumn = useCallback(
|
||||
(columnKey: string) => {
|
||||
const newSelectedColumns = preferences?.columns?.filter(
|
||||
({ name }) => name !== columnKey,
|
||||
(k) => buildAttributeKey(k) !== columnKey,
|
||||
);
|
||||
|
||||
if (!newSelectedColumns?.length && dataSource !== DataSource.LOGS) {
|
||||
|
||||
@@ -1,16 +1,42 @@
|
||||
import { SelectProps } from 'antd';
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
|
||||
export const buildAttributeKey = (k: TelemetryFieldKey): string =>
|
||||
`${k?.name || ''}::${k?.fieldContext || ''}::${k?.fieldDataType || ''}`;
|
||||
|
||||
export const getOptionsFromKeys = (
|
||||
keys: TelemetryFieldKey[],
|
||||
selectedKeys: (string | undefined)[],
|
||||
selectedKeys: string[],
|
||||
): SelectProps['options'] => {
|
||||
const options = keys.map(({ name }) => ({
|
||||
label: name,
|
||||
value: name,
|
||||
const options = keys.map((k) => ({
|
||||
label: k.name,
|
||||
value: buildAttributeKey(k),
|
||||
}));
|
||||
|
||||
return options.filter(
|
||||
({ value }) => !selectedKeys.find((key) => key === value),
|
||||
);
|
||||
};
|
||||
|
||||
export const parseAttributeKey = (
|
||||
key: string,
|
||||
): {
|
||||
name: string;
|
||||
fieldContext: string;
|
||||
fieldDataType: string;
|
||||
} => {
|
||||
const [name = '', fieldContext = '', fieldDataType = ''] = key.split('::');
|
||||
return { name, fieldContext, fieldDataType };
|
||||
};
|
||||
|
||||
export const getOptionLabelCounts = (
|
||||
options: Array<{ key: string }>,
|
||||
): Record<string, number> => {
|
||||
const labelCounts: Record<string, number> = {};
|
||||
(options || []).forEach(({ key }) => {
|
||||
const { name } = parseAttributeKey(key || '');
|
||||
if (!name) return;
|
||||
labelCounts[name] = (labelCounts[name] || 0) + 1;
|
||||
});
|
||||
return labelCounts;
|
||||
};
|
||||
|
||||
@@ -118,7 +118,8 @@ function UserFunction({
|
||||
if (role !== accessLevel) {
|
||||
notifications.success({
|
||||
message: 'User details updated successfully',
|
||||
description: 'The user details have been updated successfully.',
|
||||
description:
|
||||
'The user details have been updated successfully. Please request the user to logout and login again to access the platform with updated privileges.',
|
||||
});
|
||||
} else {
|
||||
notifications.success({
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { render, screen, waitFor } from 'tests/test-utils';
|
||||
|
||||
@@ -186,3 +189,68 @@ describe('Traces ListView - Error and Empty States', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('AttributeKey handling (UI)', () => {
|
||||
const BASE_URL = ENVIRONMENT.baseURL;
|
||||
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
rest.get(`${BASE_URL}/api/v1/fields/keys`, (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: {
|
||||
complete: true,
|
||||
keys: {
|
||||
attributeKeys: [
|
||||
{
|
||||
name: 'http.status_code_attribute',
|
||||
signal: 'traces',
|
||||
fieldContext: 'attribute',
|
||||
fieldDataType: 'string',
|
||||
},
|
||||
{
|
||||
name: 'http.status_code_attribute',
|
||||
signal: 'traces',
|
||||
fieldContext: 'attribute',
|
||||
fieldDataType: 'number',
|
||||
},
|
||||
{
|
||||
name: 'test options',
|
||||
signal: 'traces',
|
||||
fieldContext: 'attribute',
|
||||
fieldDataType: 'string',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('opens Options menu and shows attribute variants in the Add Column select', async () => {
|
||||
renderListView({ isFilterApplied: true });
|
||||
|
||||
verifyControlsVisibility();
|
||||
|
||||
// Open options popover
|
||||
|
||||
const optionsButton = screen.getByText(/options_menu.options|options/i);
|
||||
await userEvent.click(optionsButton);
|
||||
|
||||
const selectSearchInput = document.querySelector(
|
||||
'.ant-popover-content input.ant-select-selection-search-input',
|
||||
) as HTMLInputElement;
|
||||
|
||||
expect(selectSearchInput).toBeInTheDocument();
|
||||
await userEvent.click(selectSearchInput);
|
||||
|
||||
// Ensure dropdown items have rendered by waiting for a known option
|
||||
await screen.findByText('test options');
|
||||
|
||||
expect(screen.getAllByText('http.status_code_attribute').length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,6 @@ import APIError from 'types/api/error';
|
||||
|
||||
export const useGetPublicDashboardMeta = (
|
||||
id: string,
|
||||
enabled: boolean,
|
||||
): UseQueryResult<SuccessResponseV2<PublicDashboardMetaProps>, APIError> =>
|
||||
useQuery<SuccessResponseV2<PublicDashboardMetaProps>, APIError>({
|
||||
queryFn: () => getPublicDashboardMetaAPI({ id }),
|
||||
@@ -15,6 +14,6 @@ export const useGetPublicDashboardMeta = (
|
||||
console.error('Error getting public dashboard', error);
|
||||
},
|
||||
queryKey: [REACT_QUERY_KEY.GET_PUBLIC_DASHBOARD_META, id],
|
||||
enabled,
|
||||
enabled: !!id,
|
||||
keepPreviousData: false,
|
||||
});
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import getGlobalConfig from 'api/globalConfig/getGlobalConfig';
|
||||
import { useQuery, UseQueryResult } from 'react-query';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
import { GlobalConfigData } from 'types/api/globalConfig/types';
|
||||
|
||||
export const useGetGlobalConfig = (): UseQueryResult<
|
||||
SuccessResponseV2<GlobalConfigData>,
|
||||
APIError
|
||||
> =>
|
||||
useQuery<SuccessResponseV2<GlobalConfigData>, APIError>({
|
||||
queryKey: ['getGlobalConfig'],
|
||||
queryFn: () => getGlobalConfig(),
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
export interface GlobalConfigData {
|
||||
external_url: string;
|
||||
ingestion_url: string;
|
||||
}
|
||||
|
||||
export interface GlobalConfigDataProps {
|
||||
status: string;
|
||||
data: GlobalConfigData;
|
||||
}
|
||||
@@ -208,7 +208,6 @@ export enum QueryFunctionsTypes {
|
||||
MEDIAN_5 = 'median5',
|
||||
MEDIAN_7 = 'median7',
|
||||
TIME_SHIFT = 'timeShift',
|
||||
FILL_ZERO = 'fillZero',
|
||||
}
|
||||
|
||||
export type PanelTypeKeys =
|
||||
|
||||
@@ -59,9 +59,6 @@ describe('functionNameNormalizer', () => {
|
||||
expect(normalizeFunctionName('median5')).toBe(QueryFunctionsTypes.MEDIAN_5);
|
||||
expect(normalizeFunctionName('median7')).toBe(QueryFunctionsTypes.MEDIAN_7);
|
||||
expect(normalizeFunctionName('anomaly')).toBe(QueryFunctionsTypes.ANOMALY);
|
||||
expect(normalizeFunctionName('fillzero')).toBe(
|
||||
QueryFunctionsTypes.FILL_ZERO,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ export const normalizeFunctionName = (functionName: string): string => {
|
||||
median5: QueryFunctionsTypes.MEDIAN_5,
|
||||
median7: QueryFunctionsTypes.MEDIAN_7,
|
||||
anomaly: QueryFunctionsTypes.ANOMALY,
|
||||
fillzero: QueryFunctionsTypes.FILL_ZERO,
|
||||
};
|
||||
|
||||
// Convert to lowercase for case-insensitive matching
|
||||
|
||||
2
go.mod
2
go.mod
@@ -11,7 +11,6 @@ require (
|
||||
github.com/SigNoz/signoz-otel-collector v0.129.10-rc.9
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1
|
||||
github.com/antonmedv/expr v1.15.3
|
||||
github.com/bytedance/sonic v1.14.1
|
||||
github.com/cespare/xxhash/v2 v2.3.0
|
||||
github.com/coreos/go-oidc/v3 v3.14.1
|
||||
github.com/dgraph-io/ristretto/v2 v2.3.0
|
||||
@@ -90,6 +89,7 @@ require (
|
||||
|
||||
require (
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.14.1 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
|
||||
@@ -157,7 +157,7 @@ func New(ctx context.Context, logger *slog.Logger, registry prometheus.Registere
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return c, server.stateStore.Set(ctx, storableSilences)
|
||||
return c, server.stateStore.Set(ctx, server.orgID, storableSilences)
|
||||
})
|
||||
|
||||
}()
|
||||
@@ -186,7 +186,7 @@ func New(ctx context.Context, logger *slog.Logger, registry prometheus.Registere
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return c, server.stateStore.Set(ctx, storableNFLog)
|
||||
return c, server.stateStore.Set(ctx, server.orgID, storableNFLog)
|
||||
})
|
||||
}()
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ func (store *state) Get(ctx context.Context, orgID string) (*alertmanagertypes.S
|
||||
}
|
||||
|
||||
// Set implements alertmanagertypes.StateStore.
|
||||
func (store *state) Set(ctx context.Context, storeableState *alertmanagertypes.StoreableState) error {
|
||||
func (store *state) Set(ctx context.Context, orgID string, storeableState *alertmanagertypes.StoreableState) error {
|
||||
tx, err := store.sqlstore.BunDB().BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -55,6 +55,7 @@ func (store *state) Set(ctx context.Context, storeableState *alertmanagertypes.S
|
||||
Set("silences = EXCLUDED.silences").
|
||||
Set("nflog = EXCLUDED.nflog").
|
||||
Set("updated_at = EXCLUDED.updated_at").
|
||||
Where("org_id = ?", orgID).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -2,10 +2,9 @@ package signozalertmanager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
|
||||
"github.com/prometheus/common/model"
|
||||
"time"
|
||||
|
||||
amConfig "github.com/prometheus/alertmanager/config"
|
||||
|
||||
@@ -192,11 +191,7 @@ func (provider *provider) CreateChannel(ctx context.Context, orgID string, recei
|
||||
return err
|
||||
}
|
||||
|
||||
channel, err := alertmanagertypes.NewChannelFromReceiver(receiver, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
channel := alertmanagertypes.NewChannelFromReceiver(receiver, orgID)
|
||||
return provider.configStore.CreateChannel(ctx, channel, alertmanagertypes.WithCb(func(ctx context.Context) error {
|
||||
return provider.configStore.Set(ctx, config)
|
||||
}))
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
package signozapiserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/metricsexplorertypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func (provider *provider) addMetricsExplorerV2Routes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v2/metrics/stats", handler.New(
|
||||
provider.authZ.ViewAccess(provider.metricsExplorerHandler.GetStats),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetMetricsStats",
|
||||
Tags: []string{"metrics"},
|
||||
Summary: "Get metrics statistics",
|
||||
Description: "This endpoint provides list of metrics with their number of samples and timeseries for the given time range",
|
||||
Request: new(metricsexplorertypes.StatsRequest),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(metricsexplorertypes.StatsResponse),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/metrics/treemap", handler.New(
|
||||
provider.authZ.ViewAccess(provider.metricsExplorerHandler.GetTreemap),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetMetricsTreemap",
|
||||
Tags: []string{"metrics"},
|
||||
Summary: "Get metrics treemap",
|
||||
Description: "This endpoint returns a treemap visualization showing the proportional distribution of metrics by sample count or time series count",
|
||||
Request: new(metricsexplorertypes.TreemapRequest),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(metricsexplorertypes.TreemapResponse),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/metrics/attributes", handler.New(
|
||||
provider.authZ.ViewAccess(provider.metricsExplorerHandler.GetMetricAttributes),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetMetricAttributes",
|
||||
Tags: []string{"metrics"},
|
||||
Summary: "Get metric attributes",
|
||||
Description: "This endpoint returns attribute keys and their unique values for a specified metric",
|
||||
Request: new(metricsexplorertypes.MetricAttributesRequest),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(metricsexplorertypes.MetricAttributesResponse),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/metrics/metadata", handler.New(
|
||||
provider.authZ.ViewAccess(provider.metricsExplorerHandler.GetMetricMetadata),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetMetricMetadata",
|
||||
Tags: []string{"metrics"},
|
||||
Summary: "Get metric metadata",
|
||||
Description: "This endpoint returns metadata information like metric description, unit, type, temporality, monotonicity for a specified metric",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(metricsexplorertypes.MetricMetadata),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusNotFound, http.StatusInternalServerError},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/metrics/{metric_name}/metadata", handler.New(
|
||||
provider.authZ.EditAccess(provider.metricsExplorerHandler.UpdateMetricMetadata),
|
||||
handler.OpenAPIDef{
|
||||
ID: "UpdateMetricMetadata",
|
||||
Tags: []string{"metrics"},
|
||||
Summary: "Update metric metadata",
|
||||
Description: "This endpoint helps to update metadata information like metric description, unit, type, temporality, monotonicity for a specified metric",
|
||||
Request: new(metricsexplorertypes.UpdateMetricMetadataRequest),
|
||||
RequestContentType: "application/json",
|
||||
Response: nil,
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/metric/highlights", handler.New(
|
||||
provider.authZ.ViewAccess(provider.metricsExplorerHandler.GetMetricHighlights),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetMetricHighlights",
|
||||
Tags: []string{"metrics"},
|
||||
Summary: "Get metric highlights",
|
||||
Description: "This endpoint returns highlights like number of datapoints, totaltimeseries, active time series, last received time for a specified metric",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(metricsexplorertypes.MetricHighlightsResponse),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/metric/alerts", handler.New(
|
||||
provider.authZ.ViewAccess(provider.metricsExplorerHandler.GetMetricAlerts),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetMetricAlerts",
|
||||
Tags: []string{"metrics"},
|
||||
Summary: "Get metric alerts",
|
||||
Description: "This endpoint returns associated alerts for a specified metric",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(metricsexplorertypes.MetricAlertsResponse),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/metric/dashboards", handler.New(
|
||||
provider.authZ.ViewAccess(provider.metricsExplorerHandler.GetMetricDashboards),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetMetricDashboards",
|
||||
Tags: []string{"metrics"},
|
||||
Summary: "Get metric dashboards",
|
||||
Description: "This endpoint returns associated dashboards for a specified metric",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(metricsexplorertypes.MetricDashboardsResponse),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusInternalServerError},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/http/middleware"
|
||||
"github.com/SigNoz/signoz/pkg/modules/authdomain"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/preference"
|
||||
"github.com/SigNoz/signoz/pkg/modules/promote"
|
||||
@@ -24,21 +23,20 @@ import (
|
||||
)
|
||||
|
||||
type provider struct {
|
||||
config apiserver.Config
|
||||
settings factory.ScopedProviderSettings
|
||||
router *mux.Router
|
||||
authZ *middleware.AuthZ
|
||||
orgHandler organization.Handler
|
||||
userHandler user.Handler
|
||||
sessionHandler session.Handler
|
||||
authDomainHandler authdomain.Handler
|
||||
preferenceHandler preference.Handler
|
||||
globalHandler global.Handler
|
||||
promoteHandler promote.Handler
|
||||
flaggerHandler flagger.Handler
|
||||
dashboardModule dashboard.Module
|
||||
dashboardHandler dashboard.Handler
|
||||
metricsExplorerHandler metricsexplorer.Handler
|
||||
config apiserver.Config
|
||||
settings factory.ScopedProviderSettings
|
||||
router *mux.Router
|
||||
authZ *middleware.AuthZ
|
||||
orgHandler organization.Handler
|
||||
userHandler user.Handler
|
||||
sessionHandler session.Handler
|
||||
authDomainHandler authdomain.Handler
|
||||
preferenceHandler preference.Handler
|
||||
globalHandler global.Handler
|
||||
promoteHandler promote.Handler
|
||||
flaggerHandler flagger.Handler
|
||||
dashboardModule dashboard.Module
|
||||
dashboardHandler dashboard.Handler
|
||||
}
|
||||
|
||||
func NewFactory(
|
||||
@@ -54,10 +52,9 @@ func NewFactory(
|
||||
flaggerHandler flagger.Handler,
|
||||
dashboardModule dashboard.Module,
|
||||
dashboardHandler dashboard.Handler,
|
||||
metricsExplorerHandler metricsexplorer.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(ctx, providerSettings, config, orgGetter, authz, orgHandler, userHandler, sessionHandler, authDomainHandler, preferenceHandler, globalHandler, promoteHandler, flaggerHandler, dashboardModule, dashboardHandler, metricsExplorerHandler)
|
||||
return newProvider(ctx, providerSettings, config, orgGetter, authz, orgHandler, userHandler, sessionHandler, authDomainHandler, preferenceHandler, globalHandler, promoteHandler, flaggerHandler, dashboardModule, dashboardHandler)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -77,26 +74,24 @@ func newProvider(
|
||||
flaggerHandler flagger.Handler,
|
||||
dashboardModule dashboard.Module,
|
||||
dashboardHandler dashboard.Handler,
|
||||
metricsExplorerHandler metricsexplorer.Handler,
|
||||
) (apiserver.APIServer, error) {
|
||||
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/apiserver/signozapiserver")
|
||||
router := mux.NewRouter().UseEncodedPath()
|
||||
|
||||
provider := &provider{
|
||||
config: config,
|
||||
settings: settings,
|
||||
router: router,
|
||||
orgHandler: orgHandler,
|
||||
userHandler: userHandler,
|
||||
sessionHandler: sessionHandler,
|
||||
authDomainHandler: authDomainHandler,
|
||||
preferenceHandler: preferenceHandler,
|
||||
globalHandler: globalHandler,
|
||||
promoteHandler: promoteHandler,
|
||||
flaggerHandler: flaggerHandler,
|
||||
dashboardModule: dashboardModule,
|
||||
dashboardHandler: dashboardHandler,
|
||||
metricsExplorerHandler: metricsExplorerHandler,
|
||||
config: config,
|
||||
settings: settings,
|
||||
router: router,
|
||||
orgHandler: orgHandler,
|
||||
userHandler: userHandler,
|
||||
sessionHandler: sessionHandler,
|
||||
authDomainHandler: authDomainHandler,
|
||||
preferenceHandler: preferenceHandler,
|
||||
globalHandler: globalHandler,
|
||||
promoteHandler: promoteHandler,
|
||||
flaggerHandler: flaggerHandler,
|
||||
dashboardModule: dashboardModule,
|
||||
dashboardHandler: dashboardHandler,
|
||||
}
|
||||
|
||||
provider.authZ = middleware.NewAuthZ(settings.Logger(), orgGetter, authz)
|
||||
@@ -149,10 +144,6 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := provider.addMetricsExplorerV2Routes(router); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -18,37 +18,11 @@ type FlaggerProvider interface {
|
||||
|
||||
// This is the consumer facing interface for the Flagger service.
|
||||
type Flagger interface {
|
||||
// Returns value for the flag of kind boolean otherwise returns error
|
||||
Boolean(ctx context.Context, flag featuretypes.Name, evalCtx featuretypes.FlaggerEvaluationContext) (bool, error)
|
||||
|
||||
// Returns value for the flag of kind string otherwise returns error
|
||||
String(ctx context.Context, flag featuretypes.Name, evalCtx featuretypes.FlaggerEvaluationContext) (string, error)
|
||||
|
||||
// Returns value for the flag of kind float otherwise returns error
|
||||
Float(ctx context.Context, flag featuretypes.Name, evalCtx featuretypes.FlaggerEvaluationContext) (float64, error)
|
||||
|
||||
// Returns value for the flag of kind int otherwise returns error
|
||||
Int(ctx context.Context, flag featuretypes.Name, evalCtx featuretypes.FlaggerEvaluationContext) (int64, error)
|
||||
|
||||
// Returns value for the flag of kind object otherwise returns error
|
||||
Object(ctx context.Context, flag featuretypes.Name, evalCtx featuretypes.FlaggerEvaluationContext) (any, error)
|
||||
|
||||
// Returns value for the flag of kind boolean otherwise returns empty (default) value
|
||||
BooleanOrEmpty(ctx context.Context, flag featuretypes.Name, evalCtx featuretypes.FlaggerEvaluationContext) bool
|
||||
|
||||
// Returns value for the flag of kind string otherwise returns empty (default) value
|
||||
StringOrEmpty(ctx context.Context, flag featuretypes.Name, evalCtx featuretypes.FlaggerEvaluationContext) string
|
||||
|
||||
// Returns value for the flag of kind float otherwise returns empty (default) value
|
||||
FloatOrEmpty(ctx context.Context, flag featuretypes.Name, evalCtx featuretypes.FlaggerEvaluationContext) float64
|
||||
|
||||
// Returns value for the flag of kind int otherwise returns empty (default) value
|
||||
IntOrEmpty(ctx context.Context, flag featuretypes.Name, evalCtx featuretypes.FlaggerEvaluationContext) int64
|
||||
|
||||
// Returns value for the flag of kind object otherwise returns empty (default) value
|
||||
ObjectOrEmpty(ctx context.Context, flag featuretypes.Name, evalCtx featuretypes.FlaggerEvaluationContext) any
|
||||
|
||||
// Returns all the features in the registry
|
||||
Boolean(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (bool, error)
|
||||
String(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (string, error)
|
||||
Float(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (float64, error)
|
||||
Int(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (int64, error)
|
||||
Object(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (any, error)
|
||||
List(ctx context.Context, evalCtx featuretypes.FlaggerEvaluationContext) ([]*featuretypes.GettableFeature, error)
|
||||
}
|
||||
|
||||
@@ -91,9 +65,9 @@ func New(ctx context.Context, ps factory.ProviderSettings, config Config, regist
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *flagger) Boolean(ctx context.Context, flag featuretypes.Name, evalCtx featuretypes.FlaggerEvaluationContext) (bool, error) {
|
||||
func (f *flagger) Boolean(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (bool, error) {
|
||||
// check if the feature is present in the default registry
|
||||
feature, _, err := f.registry.Get(flag)
|
||||
feature, _, err := f.registry.GetByString(flag)
|
||||
if err != nil {
|
||||
f.settings.Logger().ErrorContext(ctx, "failed to get feature from default registry", "error", err, "flag", flag)
|
||||
return false, err
|
||||
@@ -110,7 +84,7 @@ func (f *flagger) Boolean(ctx context.Context, flag featuretypes.Name, evalCtx f
|
||||
// * this logic can be optimised based on priority of the clients and short circuiting
|
||||
// now ask all the available clients for the value
|
||||
for _, client := range f.clients {
|
||||
value, err := client.BooleanValue(ctx, flag.String(), defaultValue, evalCtx.Ctx())
|
||||
value, err := client.BooleanValue(ctx, flag, 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)
|
||||
continue
|
||||
@@ -124,9 +98,9 @@ func (f *flagger) Boolean(ctx context.Context, flag featuretypes.Name, evalCtx f
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
func (f *flagger) String(ctx context.Context, flag featuretypes.Name, evalCtx featuretypes.FlaggerEvaluationContext) (string, error) {
|
||||
func (f *flagger) String(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (string, error) {
|
||||
// check if the feature is present in the default registry
|
||||
feature, _, err := f.registry.Get(flag)
|
||||
feature, _, err := f.registry.GetByString(flag)
|
||||
if err != nil {
|
||||
f.settings.Logger().ErrorContext(ctx, "failed to get feature from default registry", "error", err, "flag", flag)
|
||||
return "", err
|
||||
@@ -143,7 +117,7 @@ func (f *flagger) String(ctx context.Context, flag featuretypes.Name, evalCtx fe
|
||||
// * this logic can be optimised based on priority of the clients and short circuiting
|
||||
// now ask all the available clients for the value
|
||||
for _, client := range f.clients {
|
||||
value, err := client.StringValue(ctx, flag.String(), defaultValue, evalCtx.Ctx())
|
||||
value, err := client.StringValue(ctx, flag, 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)
|
||||
continue
|
||||
@@ -157,9 +131,9 @@ func (f *flagger) String(ctx context.Context, flag featuretypes.Name, evalCtx fe
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
func (f *flagger) Float(ctx context.Context, flag featuretypes.Name, evalCtx featuretypes.FlaggerEvaluationContext) (float64, error) {
|
||||
func (f *flagger) Float(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (float64, error) {
|
||||
// check if the feature is present in the default registry
|
||||
feature, _, err := f.registry.Get(flag)
|
||||
feature, _, err := f.registry.GetByString(flag)
|
||||
if err != nil {
|
||||
f.settings.Logger().ErrorContext(ctx, "failed to get feature from default registry", "error", err, "flag", flag)
|
||||
return 0, err
|
||||
@@ -176,7 +150,7 @@ func (f *flagger) Float(ctx context.Context, flag featuretypes.Name, evalCtx fea
|
||||
// * this logic can be optimised based on priority of the clients and short circuiting
|
||||
// now ask all the available clients for the value
|
||||
for _, client := range f.clients {
|
||||
value, err := client.FloatValue(ctx, flag.String(), defaultValue, evalCtx.Ctx())
|
||||
value, err := client.FloatValue(ctx, flag, 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)
|
||||
continue
|
||||
@@ -190,9 +164,9 @@ func (f *flagger) Float(ctx context.Context, flag featuretypes.Name, evalCtx fea
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
func (f *flagger) Int(ctx context.Context, flag featuretypes.Name, evalCtx featuretypes.FlaggerEvaluationContext) (int64, error) {
|
||||
func (f *flagger) Int(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (int64, error) {
|
||||
// check if the feature is present in the default registry
|
||||
feature, _, err := f.registry.Get(flag)
|
||||
feature, _, err := f.registry.GetByString(flag)
|
||||
if err != nil {
|
||||
f.settings.Logger().ErrorContext(ctx, "failed to get feature from default registry", "error", err, "flag", flag)
|
||||
return 0, err
|
||||
@@ -209,7 +183,7 @@ func (f *flagger) Int(ctx context.Context, flag featuretypes.Name, evalCtx featu
|
||||
// * this logic can be optimised based on priority of the clients and short circuiting
|
||||
// now ask all the available clients for the value
|
||||
for _, client := range f.clients {
|
||||
value, err := client.IntValue(ctx, flag.String(), defaultValue, evalCtx.Ctx())
|
||||
value, err := client.IntValue(ctx, flag, 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)
|
||||
continue
|
||||
@@ -223,9 +197,9 @@ func (f *flagger) Int(ctx context.Context, flag featuretypes.Name, evalCtx featu
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
func (f *flagger) Object(ctx context.Context, flag featuretypes.Name, evalCtx featuretypes.FlaggerEvaluationContext) (any, error) {
|
||||
func (f *flagger) Object(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (any, error) {
|
||||
// check if the feature is present in the default registry
|
||||
feature, _, err := f.registry.Get(flag)
|
||||
feature, _, err := f.registry.GetByString(flag)
|
||||
if err != nil {
|
||||
f.settings.Logger().ErrorContext(ctx, "failed to get feature from default registry", "error", err, "flag", flag)
|
||||
return nil, err
|
||||
@@ -242,7 +216,7 @@ func (f *flagger) Object(ctx context.Context, flag featuretypes.Name, evalCtx fe
|
||||
// * this logic can be optimised based on priority of the clients and short circuiting
|
||||
// now ask all the available clients for the value
|
||||
for _, client := range f.clients {
|
||||
value, err := client.ObjectValue(ctx, flag.String(), defaultValue, evalCtx.Ctx())
|
||||
value, err := client.ObjectValue(ctx, flag, 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)
|
||||
continue
|
||||
@@ -258,56 +232,6 @@ func (f *flagger) Object(ctx context.Context, flag featuretypes.Name, evalCtx fe
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
func (f *flagger) BooleanOrEmpty(ctx context.Context, flag featuretypes.Name, evalCtx featuretypes.FlaggerEvaluationContext) bool {
|
||||
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)
|
||||
return defaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (f *flagger) StringOrEmpty(ctx context.Context, flag featuretypes.Name, evalCtx featuretypes.FlaggerEvaluationContext) string {
|
||||
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)
|
||||
return defaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (f *flagger) FloatOrEmpty(ctx context.Context, flag featuretypes.Name, evalCtx featuretypes.FlaggerEvaluationContext) float64 {
|
||||
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)
|
||||
return defaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (f *flagger) IntOrEmpty(ctx context.Context, flag featuretypes.Name, evalCtx featuretypes.FlaggerEvaluationContext) int64 {
|
||||
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)
|
||||
return defaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (f *flagger) ObjectOrEmpty(ctx context.Context, flag featuretypes.Name, evalCtx featuretypes.FlaggerEvaluationContext) any {
|
||||
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)
|
||||
return defaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (f *flagger) List(ctx context.Context, evalCtx featuretypes.FlaggerEvaluationContext) ([]*featuretypes.GettableFeature, error) {
|
||||
// get all the feature from the default registry
|
||||
allFeatures := f.registry.List()
|
||||
|
||||
@@ -2,39 +2,8 @@ package flagger
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
|
||||
var (
|
||||
FeatureUseSpanMetrics = featuretypes.MustNewName("use_span_metrics")
|
||||
FeatureInterpolationEnabled = featuretypes.MustNewName("interpolation_enabled")
|
||||
FeatureKafkaSpanEval = featuretypes.MustNewName("kafka_span_eval")
|
||||
)
|
||||
|
||||
func MustNewRegistry() featuretypes.Registry {
|
||||
registry, err := featuretypes.NewRegistry(
|
||||
&featuretypes.Feature{
|
||||
Name: FeatureUseSpanMetrics,
|
||||
Kind: featuretypes.KindBoolean,
|
||||
Stage: featuretypes.StageStable,
|
||||
Description: "Controls whether to use span metrics",
|
||||
DefaultVariant: featuretypes.MustNewName("disabled"),
|
||||
Variants: featuretypes.NewBooleanVariants(),
|
||||
},
|
||||
&featuretypes.Feature{
|
||||
Name: FeatureInterpolationEnabled,
|
||||
Kind: featuretypes.KindBoolean,
|
||||
Stage: featuretypes.StageExperimental,
|
||||
Description: "Controls whether to enable interpolation",
|
||||
DefaultVariant: featuretypes.MustNewName("disabled"),
|
||||
Variants: featuretypes.NewBooleanVariants(),
|
||||
},
|
||||
&featuretypes.Feature{
|
||||
Name: FeatureKafkaSpanEval,
|
||||
Kind: featuretypes.KindBoolean,
|
||||
Stage: featuretypes.StageExperimental,
|
||||
Description: "Controls whether to enable kafka span eval",
|
||||
DefaultVariant: featuretypes.MustNewName("disabled"),
|
||||
Variants: featuretypes.NewBooleanVariants(),
|
||||
},
|
||||
)
|
||||
registry, err := featuretypes.NewRegistry()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ func (m *module) ListPromotedAndIndexedPaths(ctx context.Context) ([]promotetype
|
||||
response := []promotetypes.PromotePath{}
|
||||
for _, path := range promotedPaths {
|
||||
fullPath := telemetrylogs.BodyPromotedColumnPrefix + path
|
||||
path = telemetrytypes.BodyJSONStringSearchPrefix + path
|
||||
path = telemetrylogs.BodyJSONStringSearchPrefix + path
|
||||
item := promotetypes.PromotePath{
|
||||
Path: path,
|
||||
Promote: true,
|
||||
@@ -77,7 +77,7 @@ func (m *module) ListPromotedAndIndexedPaths(ctx context.Context) ([]promotetype
|
||||
// add the paths that are not promoted but have indexes
|
||||
for path, indexes := range aggr {
|
||||
path := strings.TrimPrefix(path, telemetrylogs.BodyJSONColumnPrefix)
|
||||
path = telemetrytypes.BodyJSONStringSearchPrefix + path
|
||||
path = telemetrylogs.BodyJSONStringSearchPrefix + path
|
||||
response = append(response, promotetypes.PromotePath{
|
||||
Path: path,
|
||||
Indexes: indexes,
|
||||
|
||||
@@ -10,11 +10,9 @@ import (
|
||||
|
||||
"github.com/ClickHouse/clickhouse-go/v2"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrylogs"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/bytedance/sonic"
|
||||
)
|
||||
|
||||
type builderQuery[T any] struct {
|
||||
@@ -250,40 +248,6 @@ func (q *builderQuery[T]) executeWithContext(ctx context.Context, query string,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// merge body_json and promoted into body
|
||||
if q.spec.Signal == telemetrytypes.SignalLogs {
|
||||
switch typedPayload := payload.(type) {
|
||||
case *qbtypes.RawData:
|
||||
for _, rr := range typedPayload.Rows {
|
||||
seeder := func() error {
|
||||
body, ok := rr.Data[telemetrylogs.LogsV2BodyJSONColumn].(map[string]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
promoted, ok := rr.Data[telemetrylogs.LogsV2BodyPromotedColumn].(map[string]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
seed(promoted, body)
|
||||
str, err := sonic.MarshalString(body)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to marshal body")
|
||||
}
|
||||
rr.Data["body"] = str
|
||||
return nil
|
||||
}
|
||||
err := seeder()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
delete(rr.Data, telemetrylogs.LogsV2BodyJSONColumn)
|
||||
delete(rr.Data, telemetrylogs.LogsV2BodyPromotedColumn)
|
||||
}
|
||||
payload = typedPayload
|
||||
}
|
||||
}
|
||||
|
||||
return &qbtypes.Result{
|
||||
Type: q.kind,
|
||||
Value: payload,
|
||||
@@ -411,18 +375,3 @@ func decodeCursor(cur string) (int64, error) {
|
||||
}
|
||||
return strconv.ParseInt(string(b), 10, 64)
|
||||
}
|
||||
|
||||
func seed(promoted map[string]any, body map[string]any) {
|
||||
for key, fromValue := range promoted {
|
||||
if toValue, ok := body[key]; !ok {
|
||||
body[key] = fromValue
|
||||
} else {
|
||||
if fromValue, ok := fromValue.(map[string]any); ok {
|
||||
if toValue, ok := toValue.(map[string]any); ok {
|
||||
seed(fromValue, toValue)
|
||||
body[key] = toValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/bytedance/sonic"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -52,6 +51,7 @@ func consume(rows driver.Rows, kind qbtypes.RequestType, queryWindow *qbtypes.Ti
|
||||
}
|
||||
|
||||
func readAsTimeSeries(rows driver.Rows, queryWindow *qbtypes.TimeRange, step qbtypes.Step, queryName string) (*qbtypes.TimeSeriesData, error) {
|
||||
|
||||
colTypes := rows.ColumnTypes()
|
||||
colNames := rows.Columns()
|
||||
|
||||
@@ -354,22 +354,10 @@ func readAsRaw(rows driver.Rows, queryName string) (*qbtypes.RawData, error) {
|
||||
colTypes := rows.ColumnTypes()
|
||||
colCnt := len(colNames)
|
||||
|
||||
// Helper that decides scan target per column based on DB type
|
||||
makeScanTarget := func(i int) any {
|
||||
dbt := strings.ToUpper(colTypes[i].DatabaseTypeName())
|
||||
if strings.HasPrefix(dbt, "JSON") {
|
||||
// Since the driver fails to decode JSON/Dynamic into native Go values, we read it as raw bytes
|
||||
// TODO: check in future if fixed in the driver
|
||||
var v []byte
|
||||
return &v
|
||||
}
|
||||
return reflect.New(colTypes[i].ScanType()).Interface()
|
||||
}
|
||||
|
||||
// Build a template slice of correctly-typed pointers once
|
||||
scanTpl := make([]any, colCnt)
|
||||
for i := range colTypes {
|
||||
scanTpl[i] = makeScanTarget(i)
|
||||
for i, ct := range colTypes {
|
||||
scanTpl[i] = reflect.New(ct.ScanType()).Interface()
|
||||
}
|
||||
|
||||
var outRows []*qbtypes.RawRow
|
||||
@@ -378,7 +366,7 @@ func readAsRaw(rows driver.Rows, queryName string) (*qbtypes.RawData, error) {
|
||||
// fresh copy of the scan slice (otherwise the driver reuses pointers)
|
||||
scan := make([]any, colCnt)
|
||||
for i := range scanTpl {
|
||||
scan[i] = makeScanTarget(i)
|
||||
scan[i] = reflect.New(colTypes[i].ScanType()).Interface()
|
||||
}
|
||||
|
||||
if err := rows.Scan(scan...); err != nil {
|
||||
@@ -395,21 +383,6 @@ func readAsRaw(rows driver.Rows, queryName string) (*qbtypes.RawData, error) {
|
||||
// de-reference the typed pointer to any
|
||||
val := reflect.ValueOf(cellPtr).Elem().Interface()
|
||||
|
||||
// Post-process JSON columns: normalize into structured values
|
||||
if strings.HasPrefix(strings.ToUpper(colTypes[i].DatabaseTypeName()), "JSON") {
|
||||
switch x := val.(type) {
|
||||
case []byte:
|
||||
if len(x) > 0 {
|
||||
var v any
|
||||
if err := sonic.Unmarshal(x, &v); err == nil {
|
||||
val = v
|
||||
}
|
||||
}
|
||||
default:
|
||||
// already a structured type (map[string]any, []any, etc.)
|
||||
}
|
||||
}
|
||||
|
||||
// special-case: timestamp column
|
||||
if name == "timestamp" || name == "timestamp_datetime" {
|
||||
switch t := val.(type) {
|
||||
|
||||
@@ -593,31 +593,19 @@ func (q *querier) run(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// attach step interval to metadata so client can make informed decisions, ex: width of the bar
|
||||
// or go to related logs/traces from a point in line/bar chart with correct time range
|
||||
stepIntervals := make(map[string]uint64, len(steps))
|
||||
for name, step := range steps {
|
||||
stepIntervals[name] = uint64(step.Duration.Seconds())
|
||||
}
|
||||
for _, query := range req.CompositeQuery.Queries {
|
||||
if query.Type == qbtypes.QueryTypeFormula {
|
||||
if formula, ok := query.Spec.(qbtypes.QueryBuilderFormula); ok {
|
||||
formulaStepMs := q.calculateFormulaStep(formula.Expression, req)
|
||||
stepIntervals[formula.Name] = uint64(formulaStepMs / 1000) // convert ms to seconds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resp := &qbtypes.QueryRangeResponse{
|
||||
Type: req.RequestType,
|
||||
Data: qbtypes.QueryData{
|
||||
Results: maps.Values(processedResults),
|
||||
},
|
||||
Meta: qbtypes.ExecStats{
|
||||
RowsScanned: stats.RowsScanned,
|
||||
BytesScanned: stats.BytesScanned,
|
||||
DurationMS: stats.DurationMS,
|
||||
StepIntervals: stepIntervals,
|
||||
Meta: struct {
|
||||
RowsScanned uint64 `json:"rowsScanned"`
|
||||
BytesScanned uint64 `json:"bytesScanned"`
|
||||
DurationMS uint64 `json:"durationMs"`
|
||||
}{
|
||||
RowsScanned: stats.RowsScanned,
|
||||
BytesScanned: stats.BytesScanned,
|
||||
DurationMS: stats.DurationMS,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/cache"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
@@ -23,7 +22,6 @@ func NewFactory(
|
||||
telemetryStore telemetrystore.TelemetryStore,
|
||||
prometheus prometheus.Prometheus,
|
||||
cache cache.Cache,
|
||||
flagger flagger.Flagger,
|
||||
) factory.ProviderFactory[querier.Querier, querier.Config] {
|
||||
return factory.NewProviderFactory(
|
||||
factory.MustNewName("signoz"),
|
||||
@@ -32,7 +30,7 @@ func NewFactory(
|
||||
settings factory.ProviderSettings,
|
||||
cfg querier.Config,
|
||||
) (querier.Querier, error) {
|
||||
return newProvider(ctx, settings, cfg, telemetryStore, prometheus, cache, flagger)
|
||||
return newProvider(ctx, settings, cfg, telemetryStore, prometheus, cache)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -44,7 +42,6 @@ func newProvider(
|
||||
telemetryStore telemetrystore.TelemetryStore,
|
||||
prometheus prometheus.Prometheus,
|
||||
cache cache.Cache,
|
||||
flagger flagger.Flagger,
|
||||
) (querier.Querier, error) {
|
||||
|
||||
// Create telemetry metadata store
|
||||
@@ -81,7 +78,7 @@ func newProvider(
|
||||
telemetryMetadataStore,
|
||||
)
|
||||
|
||||
traceAggExprRewriter := querybuilder.NewAggExprRewriter(settings, nil, traceFieldMapper, traceConditionBuilder, nil)
|
||||
traceAggExprRewriter := querybuilder.NewAggExprRewriter(settings, nil, traceFieldMapper, traceConditionBuilder, "", nil)
|
||||
traceStmtBuilder := telemetrytraces.NewTraceQueryStatementBuilder(
|
||||
settings,
|
||||
telemetryMetadataStore,
|
||||
@@ -105,13 +102,14 @@ func newProvider(
|
||||
|
||||
// Create log statement builder
|
||||
logFieldMapper := telemetrylogs.NewFieldMapper()
|
||||
logConditionBuilder := telemetrylogs.NewConditionBuilder(logFieldMapper, telemetryMetadataStore)
|
||||
logConditionBuilder := telemetrylogs.NewConditionBuilder(logFieldMapper)
|
||||
logResourceFilterStmtBuilder := resourcefilter.NewLogResourceFilterStatementBuilder(
|
||||
settings,
|
||||
resourceFilterFieldMapper,
|
||||
resourceFilterConditionBuilder,
|
||||
telemetryMetadataStore,
|
||||
telemetrylogs.DefaultFullTextColumn,
|
||||
telemetrylogs.BodyJSONStringSearchPrefix,
|
||||
telemetrylogs.GetBodyJSONKey,
|
||||
)
|
||||
logAggExprRewriter := querybuilder.NewAggExprRewriter(
|
||||
@@ -119,6 +117,7 @@ func newProvider(
|
||||
telemetrylogs.DefaultFullTextColumn,
|
||||
logFieldMapper,
|
||||
logConditionBuilder,
|
||||
telemetrylogs.BodyJSONStringSearchPrefix,
|
||||
telemetrylogs.GetBodyJSONKey,
|
||||
)
|
||||
logStmtBuilder := telemetrylogs.NewLogQueryStatementBuilder(
|
||||
@@ -129,6 +128,7 @@ func newProvider(
|
||||
logResourceFilterStmtBuilder,
|
||||
logAggExprRewriter,
|
||||
telemetrylogs.DefaultFullTextColumn,
|
||||
telemetrylogs.BodyJSONStringSearchPrefix,
|
||||
telemetrylogs.GetBodyJSONKey,
|
||||
)
|
||||
|
||||
@@ -140,7 +140,6 @@ func newProvider(
|
||||
telemetryMetadataStore,
|
||||
metricFieldMapper,
|
||||
metricConditionBuilder,
|
||||
flagger,
|
||||
)
|
||||
|
||||
// Create meter statement builder
|
||||
@@ -149,7 +148,6 @@ func newProvider(
|
||||
telemetryMetadataStore,
|
||||
metricFieldMapper,
|
||||
metricConditionBuilder,
|
||||
metricStmtBuilder,
|
||||
)
|
||||
|
||||
// Create bucket cache
|
||||
|
||||
@@ -934,8 +934,6 @@ func (r *ClickHouseReader) GetWaterfallSpansForTraceWithMetadata(ctx context.Con
|
||||
events = append(events, eventMap)
|
||||
}
|
||||
|
||||
startTimeUnixNano := uint64(item.TimeUnixNano.UnixNano())
|
||||
|
||||
jsonItem := model.Span{
|
||||
SpanID: item.SpanID,
|
||||
TraceID: item.TraceID,
|
||||
@@ -951,10 +949,10 @@ func (r *ClickHouseReader) GetWaterfallSpansForTraceWithMetadata(ctx context.Con
|
||||
Events: events,
|
||||
TagMap: item.Attributes_string,
|
||||
Children: make([]*model.Span, 0),
|
||||
TimeUnixNano: startTimeUnixNano, // Store nanoseconds temporarily
|
||||
}
|
||||
|
||||
// metadata calculation
|
||||
startTimeUnixNano := uint64(item.TimeUnixNano.UnixNano())
|
||||
if startTime == 0 || startTimeUnixNano < startTime {
|
||||
startTime = startTimeUnixNano
|
||||
}
|
||||
@@ -969,9 +967,12 @@ func (r *ClickHouseReader) GetWaterfallSpansForTraceWithMetadata(ctx context.Con
|
||||
totalErrorSpans = totalErrorSpans + 1
|
||||
}
|
||||
|
||||
// convert start timestamp to millis because right now frontend is expecting it in millis
|
||||
jsonItem.TimeUnixNano = uint64(item.TimeUnixNano.UnixNano() / 1000000)
|
||||
|
||||
// collect the intervals for service for execution time calculation
|
||||
serviceNameIntervalMap[jsonItem.ServiceName] =
|
||||
append(serviceNameIntervalMap[jsonItem.ServiceName], tracedetail.Interval{StartTime: jsonItem.TimeUnixNano, Duration: jsonItem.DurationNano, Service: jsonItem.ServiceName})
|
||||
append(serviceNameIntervalMap[jsonItem.ServiceName], tracedetail.Interval{StartTime: jsonItem.TimeUnixNano, Duration: jsonItem.DurationNano / 1000000, Service: jsonItem.ServiceName})
|
||||
|
||||
// append to the span node map
|
||||
spanIdToSpanNodeMap[jsonItem.SpanID] = &jsonItem
|
||||
@@ -1049,15 +1050,6 @@ func (r *ClickHouseReader) GetWaterfallSpansForTraceWithMetadata(ctx context.Con
|
||||
selectedSpans, uncollapsedSpans, rootServiceName, rootServiceEntryPoint := tracedetail.GetSelectedSpans(req.UncollapsedSpans, req.SelectedSpanID, traceRoots, spanIdToSpanNodeMap, req.IsSelectedSpanIDUnCollapsed)
|
||||
zap.L().Info("getWaterfallSpansForTraceWithMetadata: processing post cache", zap.Duration("duration", time.Since(processingPostCache)), zap.String("traceID", traceID))
|
||||
|
||||
// convert start timestamp to millis because right now frontend is expecting it in millis
|
||||
for _, span := range selectedSpans {
|
||||
span.TimeUnixNano = span.TimeUnixNano / 1000000
|
||||
}
|
||||
|
||||
for serviceName, totalDuration := range serviceNameToTotalDurationMap {
|
||||
serviceNameToTotalDurationMap[serviceName] = totalDuration / 1000000
|
||||
}
|
||||
|
||||
response.Spans = selectedSpans
|
||||
response.UncollapsedSpans = uncollapsedSpans
|
||||
response.StartTimestampMillis = startTime / 1000000
|
||||
|
||||
@@ -116,7 +116,7 @@ func (c *Controller) GenerateConnectionUrl(ctx context.Context, orgId string, cl
|
||||
return nil, model.WrapApiError(apiErr, "couldn't upsert cloud account")
|
||||
}
|
||||
|
||||
agentVersion := "v0.0.8"
|
||||
agentVersion := "v0.0.7"
|
||||
if req.AgentConfig.Version != "" {
|
||||
agentVersion = req.AgentConfig.Version
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/modules/thirdpartyapi"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
|
||||
@@ -64,7 +63,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/licensetypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/opamptypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
|
||||
@@ -626,6 +624,15 @@ func (ah *APIHandler) MetricExplorerRoutes(router *mux.Router, am *middleware.Au
|
||||
router.HandleFunc("/api/v1/metrics/{metric_name}/metadata",
|
||||
am.ViewAccess(ah.UpdateMetricsMetadata)).
|
||||
Methods(http.MethodPost)
|
||||
// v2 endpoints
|
||||
router.HandleFunc("/api/v2/metrics/stats", am.ViewAccess(ah.Signoz.Handlers.MetricsExplorer.GetStats)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v2/metrics/treemap", am.ViewAccess(ah.Signoz.Handlers.MetricsExplorer.GetTreemap)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v2/metrics/attributes", am.ViewAccess(ah.Signoz.Handlers.MetricsExplorer.GetMetricAttributes)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v2/metrics/metadata", am.ViewAccess(ah.Signoz.Handlers.MetricsExplorer.GetMetricMetadata)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v2/metrics/{metric_name}/metadata", am.EditAccess(ah.Signoz.Handlers.MetricsExplorer.UpdateMetricMetadata)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v2/metric/highlights", am.ViewAccess(ah.Signoz.Handlers.MetricsExplorer.GetMetricHighlights)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v2/metric/alerts", am.ViewAccess(ah.Signoz.Handlers.MetricsExplorer.GetMetricAlerts)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v2/metric/dashboards", am.ViewAccess(ah.Signoz.Handlers.MetricsExplorer.GetMetricDashboards)).Methods(http.MethodGet)
|
||||
}
|
||||
|
||||
func Intersection(a, b []int) (c []int) {
|
||||
@@ -2000,25 +2007,13 @@ func (aH *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||
if err != nil {
|
||||
aH.HandleError(w, err, http.StatusInternalServerError)
|
||||
return
|
||||
if constants.PreferSpanMetrics {
|
||||
for idx, feature := range featureSet {
|
||||
if feature.Name == licensetypes.UseSpanMetrics {
|
||||
featureSet[idx].Active = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
|
||||
useSpanMetrics := aH.Signoz.Flagger.BooleanOrEmpty(r.Context(), flagger.FeatureUseSpanMetrics, evalCtx)
|
||||
|
||||
featureSet = append(featureSet, &licensetypes.Feature{
|
||||
Name: valuer.NewString(flagger.FeatureUseSpanMetrics.String()),
|
||||
Active: useSpanMetrics,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
})
|
||||
|
||||
if constants.IsDotMetricsEnabled {
|
||||
for idx, feature := range featureSet {
|
||||
if feature.Name == licensetypes.DotMetricsEnabled {
|
||||
@@ -2635,10 +2630,7 @@ func (aH *APIHandler) getProducerData(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
|
||||
kafkaSpanEval := aH.Signoz.Flagger.BooleanOrEmpty(r.Context(), flagger.FeatureKafkaSpanEval, evalCtx)
|
||||
|
||||
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "producer", kafkaSpanEval)
|
||||
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "producer")
|
||||
if err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
@@ -2688,10 +2680,7 @@ func (aH *APIHandler) getConsumerData(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
|
||||
kafkaSpanEval := aH.Signoz.Flagger.BooleanOrEmpty(r.Context(), flagger.FeatureKafkaSpanEval, evalCtx)
|
||||
|
||||
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "consumer", kafkaSpanEval)
|
||||
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "consumer")
|
||||
if err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
@@ -2742,10 +2731,7 @@ func (aH *APIHandler) getPartitionOverviewLatencyData(w http.ResponseWriter, r *
|
||||
return
|
||||
}
|
||||
|
||||
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
|
||||
kafkaSpanEval := aH.Signoz.Flagger.BooleanOrEmpty(r.Context(), flagger.FeatureKafkaSpanEval, evalCtx)
|
||||
|
||||
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "producer-topic-throughput", kafkaSpanEval)
|
||||
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "producer-topic-throughput")
|
||||
if err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
@@ -2796,10 +2782,7 @@ func (aH *APIHandler) getConsumerPartitionLatencyData(w http.ResponseWriter, r *
|
||||
return
|
||||
}
|
||||
|
||||
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
|
||||
kafkaSpanEval := aH.Signoz.Flagger.BooleanOrEmpty(r.Context(), flagger.FeatureKafkaSpanEval, evalCtx)
|
||||
|
||||
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "consumer_partition_latency", kafkaSpanEval)
|
||||
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "consumer_partition_latency")
|
||||
if err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
@@ -2964,10 +2947,7 @@ func (aH *APIHandler) getProducerThroughputDetails(w http.ResponseWriter, r *htt
|
||||
return
|
||||
}
|
||||
|
||||
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
|
||||
kafkaSpanEval := aH.Signoz.Flagger.BooleanOrEmpty(r.Context(), flagger.FeatureKafkaSpanEval, evalCtx)
|
||||
|
||||
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "producer-throughput-details", kafkaSpanEval)
|
||||
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "producer-throughput-details")
|
||||
if err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
@@ -3018,10 +2998,7 @@ func (aH *APIHandler) getConsumerThroughputOverview(w http.ResponseWriter, r *ht
|
||||
return
|
||||
}
|
||||
|
||||
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
|
||||
kafkaSpanEval := aH.Signoz.Flagger.BooleanOrEmpty(r.Context(), flagger.FeatureKafkaSpanEval, evalCtx)
|
||||
|
||||
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "consumer-throughput-overview", kafkaSpanEval)
|
||||
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "consumer-throughput-overview")
|
||||
if err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
@@ -3072,10 +3049,7 @@ func (aH *APIHandler) getConsumerThroughputDetails(w http.ResponseWriter, r *htt
|
||||
return
|
||||
}
|
||||
|
||||
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
|
||||
kafkaSpanEval := aH.Signoz.Flagger.BooleanOrEmpty(r.Context(), flagger.FeatureKafkaSpanEval, evalCtx)
|
||||
|
||||
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "consumer-throughput-details", kafkaSpanEval)
|
||||
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "consumer-throughput-details")
|
||||
if err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
@@ -3129,10 +3103,7 @@ func (aH *APIHandler) getProducerConsumerEval(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
|
||||
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
|
||||
kafkaSpanEval := aH.Signoz.Flagger.BooleanOrEmpty(r.Context(), flagger.FeatureKafkaSpanEval, evalCtx)
|
||||
|
||||
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "producer-consumer-eval", kafkaSpanEval)
|
||||
queryRangeParams, err := kafka.BuildQueryRangeParams(messagingQueue, "producer-consumer-eval")
|
||||
if err != nil {
|
||||
zap.L().Error(err.Error())
|
||||
RespondError(w, apiErr, nil)
|
||||
|
||||
@@ -2,7 +2,6 @@ package kafka
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/common"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
@@ -10,9 +9,9 @@ import (
|
||||
|
||||
var defaultStepInterval int64 = 60
|
||||
|
||||
func BuildQueryRangeParams(messagingQueue *MessagingQueue, queryContext string, kafkaSpanEval bool) (*v3.QueryRangeParamsV3, error) {
|
||||
func BuildQueryRangeParams(messagingQueue *MessagingQueue, queryContext string) (*v3.QueryRangeParamsV3, error) {
|
||||
|
||||
if !kafkaSpanEval && queryContext == "producer-consumer-eval" {
|
||||
if constants.KafkaSpanEval == "false" && queryContext == "producer-consumer-eval" {
|
||||
return nil, fmt.Errorf("span evaluation feature is disabled and is experimental")
|
||||
}
|
||||
|
||||
|
||||
@@ -11,16 +11,13 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/opamptypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -131,40 +128,6 @@ 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
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
|
||||
// pipelines and pipelines for installed integrations
|
||||
func (ic *LogParsingPipelineController) getEffectivePipelinesByVersion(
|
||||
@@ -295,13 +258,6 @@ func (pc *LogParsingPipelineController) RecommendAgentConfig(
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// recommend default pipelines along with user created pipelines
|
||||
defaultPipelines, err := pc.getDefaultPipelines()
|
||||
if err != nil {
|
||||
return nil, "", model.InternalError(fmt.Errorf("failed to get default pipelines: %w", err))
|
||||
}
|
||||
pipelinesResp.Pipelines = append(pipelinesResp.Pipelines, defaultPipelines...)
|
||||
|
||||
updatedConf, err := GenerateCollectorConfigWithPipelines(currentConfYaml, pipelinesResp.Pipelines)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
|
||||
@@ -132,7 +132,7 @@ func SignozLogsToPLogs(logs []model.SignozLog) []plog.Logs {
|
||||
slRecord.SetSeverityText(log.SeverityText)
|
||||
slRecord.SetSeverityNumber(plog.SeverityNumber(log.SeverityNumber))
|
||||
|
||||
slRecord.Body().FromRaw(log.Body)
|
||||
slRecord.Body().SetStr(log.Body)
|
||||
|
||||
slAttribs := slRecord.Attributes()
|
||||
for k, v := range log.Attributes_int64 {
|
||||
|
||||
@@ -64,9 +64,15 @@ func whichTSTableToUse(start, end int64, mq *v3.BuilderQuery) (int64, int64, str
|
||||
start = start - (start % (time.Hour.Milliseconds() * 24))
|
||||
tableName = constants.SIGNOZ_TIMESERIES_v4_1DAY_LOCAL_TABLENAME
|
||||
} else {
|
||||
// adjust the start time to nearest 1 week
|
||||
start = start - (start % (time.Hour.Milliseconds() * 24 * 7))
|
||||
tableName = constants.SIGNOZ_TIMESERIES_v4_1WEEK_LOCAL_TABLENAME
|
||||
if constants.UseMetricsPreAggregation() {
|
||||
// adjust the start time to nearest 1 week
|
||||
start = start - (start % (time.Hour.Milliseconds() * 24 * 7))
|
||||
tableName = constants.SIGNOZ_TIMESERIES_v4_1WEEK_LOCAL_TABLENAME
|
||||
} else {
|
||||
// continue to use the 1 day table
|
||||
start = start - (start % (time.Hour.Milliseconds() * 24))
|
||||
tableName = constants.SIGNOZ_TIMESERIES_v4_1DAY_LOCAL_TABLENAME
|
||||
}
|
||||
}
|
||||
|
||||
return start, end, tableName
|
||||
@@ -93,6 +99,11 @@ func WhichSamplesTableToUse(start, end int64, mq *v3.BuilderQuery) string {
|
||||
return constants.SIGNOZ_EXP_HISTOGRAM_TABLENAME
|
||||
}
|
||||
|
||||
// continue to use the old table if pre-aggregation is disabled
|
||||
if !constants.UseMetricsPreAggregation() {
|
||||
return constants.SIGNOZ_SAMPLES_V4_TABLENAME
|
||||
}
|
||||
|
||||
// if the time aggregation is count_distinct, we need to use the distributed_samples_v4 table
|
||||
// because the aggregated tables don't support count_distinct
|
||||
if mq.TimeAggregation == v3.TimeAggregationCountDistinct {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
)
|
||||
|
||||
func TestPrepareMetricQueryCumulativeRatePreAgg(t *testing.T) {
|
||||
t.Setenv("USE_METRICS_PRE_AGGREGATION", "true")
|
||||
testCases := []struct {
|
||||
name string
|
||||
builderQuery *v3.BuilderQuery
|
||||
@@ -97,6 +98,7 @@ func TestPrepareMetricQueryCumulativeRatePreAgg(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPrepareMetricQueryDeltaRatePreAgg(t *testing.T) {
|
||||
t.Setenv("USE_METRICS_PRE_AGGREGATION", "true")
|
||||
testCases := []struct {
|
||||
name string
|
||||
builderQuery *v3.BuilderQuery
|
||||
@@ -163,6 +165,7 @@ func TestPrepareMetricQueryDeltaRatePreAgg(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPrepreMetricQueryCumulativeQuantilePreAgg(t *testing.T) {
|
||||
t.Setenv("USE_METRICS_PRE_AGGREGATION", "true")
|
||||
testCases := []struct {
|
||||
name string
|
||||
builderQuery *v3.BuilderQuery
|
||||
@@ -247,6 +250,7 @@ func TestPrepreMetricQueryCumulativeQuantilePreAgg(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPrepreMetricQueryDeltaQuantilePreAgg(t *testing.T) {
|
||||
t.Setenv("USE_METRICS_PRE_AGGREGATION", "true")
|
||||
testCases := []struct {
|
||||
name string
|
||||
builderQuery *v3.BuilderQuery
|
||||
@@ -331,6 +335,7 @@ func TestPrepreMetricQueryDeltaQuantilePreAgg(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPrepareMetricQueryGaugePreAgg(t *testing.T) {
|
||||
t.Setenv("USE_METRICS_PRE_AGGREGATION", "true")
|
||||
testCases := []struct {
|
||||
name string
|
||||
builderQuery *v3.BuilderQuery
|
||||
|
||||
@@ -202,6 +202,7 @@ func TestPrepareTimeseriesFilterQuery(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPrepareMetricQueryCumulativeRate(t *testing.T) {
|
||||
t.Setenv("USE_METRICS_PRE_AGGREGATION", "false")
|
||||
testCases := []struct {
|
||||
name string
|
||||
builderQuery *v3.BuilderQuery
|
||||
@@ -290,6 +291,7 @@ func TestPrepareMetricQueryCumulativeRate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPrepareMetricQueryDeltaRate(t *testing.T) {
|
||||
t.Setenv("USE_METRICS_PRE_AGGREGATION", "false")
|
||||
testCases := []struct {
|
||||
name string
|
||||
builderQuery *v3.BuilderQuery
|
||||
@@ -356,6 +358,7 @@ func TestPrepareMetricQueryDeltaRate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPrepreMetricQueryCumulativeQuantile(t *testing.T) {
|
||||
t.Setenv("USE_METRICS_PRE_AGGREGATION", "false")
|
||||
testCases := []struct {
|
||||
name string
|
||||
builderQuery *v3.BuilderQuery
|
||||
@@ -440,6 +443,7 @@ func TestPrepreMetricQueryCumulativeQuantile(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPrepreMetricQueryDeltaQuantile(t *testing.T) {
|
||||
t.Setenv("USE_METRICS_PRE_AGGREGATION", "false")
|
||||
testCases := []struct {
|
||||
name string
|
||||
builderQuery *v3.BuilderQuery
|
||||
@@ -524,6 +528,7 @@ func TestPrepreMetricQueryDeltaQuantile(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPrepareMetricQueryGauge(t *testing.T) {
|
||||
t.Setenv("USE_METRICS_PRE_AGGREGATION", "false")
|
||||
testCases := []struct {
|
||||
name string
|
||||
builderQuery *v3.BuilderQuery
|
||||
@@ -690,6 +695,7 @@ func TestPrepareMetricQueryGauge(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPrepareMetricQueryValueTypePanelWithGroupBY(t *testing.T) {
|
||||
t.Setenv("USE_METRICS_PRE_AGGREGATION", "false")
|
||||
testCases := []struct {
|
||||
name string
|
||||
builderQuery *v3.BuilderQuery
|
||||
|
||||
@@ -819,7 +819,7 @@ func ParseQueryRangeParams(r *http.Request) (*v3.QueryRangeParamsV3, *model.ApiE
|
||||
query.StepInterval = minStep
|
||||
}
|
||||
|
||||
if query.DataSource == v3.DataSourceMetrics {
|
||||
if query.DataSource == v3.DataSourceMetrics && baseconstants.UseMetricsPreAggregation() {
|
||||
// if the time range is greater than 1 day, and less than 1 week set the step interval to be multiple of 5 minutes
|
||||
// if the time range is greater than 1 week, set the step interval to be multiple of 30 mins
|
||||
start, end := queryRangeParams.Start, queryRangeParams.End
|
||||
|
||||
@@ -40,6 +40,13 @@ const NormalizedMetricsMapQueryThreads = 10
|
||||
var NormalizedMetricsMapRegex = regexp.MustCompile(`[^a-zA-Z0-9]`)
|
||||
var NormalizedMetricsMapQuantileRegex = regexp.MustCompile(`(?i)([._-]?quantile.*)$`)
|
||||
|
||||
// TODO(srikanthccv): remove after backfilling is done
|
||||
func UseMetricsPreAggregation() bool {
|
||||
return GetOrDefaultEnv("USE_METRICS_PRE_AGGREGATION", "true") == "true"
|
||||
}
|
||||
|
||||
var KafkaSpanEval = GetOrDefaultEnv("KAFKA_SPAN_EVAL", "false")
|
||||
|
||||
func GetEvalDelay() time.Duration {
|
||||
evalDelayStr := GetOrDefaultEnv("RULES_EVAL_DELAY", "2m")
|
||||
evalDelayDuration, err := time.ParseDuration(evalDelayStr)
|
||||
@@ -678,6 +685,7 @@ var OldToNewTraceFieldsMap = map[string]string{
|
||||
var StaticFieldsTraces = map[string]v3.AttributeKey{}
|
||||
|
||||
var IsDotMetricsEnabled = false
|
||||
var PreferSpanMetrics = false
|
||||
var MaxJSONFlatteningDepth = 1
|
||||
|
||||
func init() {
|
||||
@@ -686,6 +694,9 @@ func init() {
|
||||
if GetOrDefaultEnv(DotMetricsEnabled, "true") == "true" {
|
||||
IsDotMetricsEnabled = true
|
||||
}
|
||||
if GetOrDefaultEnv("USE_SPAN_METRICS", "false") == "true" {
|
||||
PreferSpanMetrics = true
|
||||
}
|
||||
|
||||
// set max flattening depth
|
||||
depth, err := strconv.Atoi(GetOrDefaultEnv(maxJSONFlatteningDepth, "1"))
|
||||
|
||||
@@ -764,10 +764,8 @@ func (m *Manager) prepareTestNotifyFunc() NotifyFunc {
|
||||
a := &alertmanagertypes.PostableAlert{}
|
||||
a.Annotations = alert.Annotations.Map()
|
||||
a.StartsAt = strfmt.DateTime(alert.FiredAt)
|
||||
labelsMap := alert.Labels.Map()
|
||||
labelsMap[labels.TestAlertLabel] = "true"
|
||||
a.Alert = alertmanagertypes.AlertModel{
|
||||
Labels: labelsMap,
|
||||
Labels: alert.Labels.Map(),
|
||||
GeneratorURL: strfmt.URI(generatorURL),
|
||||
}
|
||||
if !alert.ResolvedAt.IsZero() {
|
||||
|
||||
@@ -166,7 +166,7 @@ func (r *ThresholdRule) prepareQueryRange(ctx context.Context, ts time.Time) (*v
|
||||
|
||||
q.SetShiftByFromFunc()
|
||||
|
||||
if q.DataSource == v3.DataSourceMetrics {
|
||||
if q.DataSource == v3.DataSourceMetrics && constants.UseMetricsPreAggregation() {
|
||||
// if the time range is greater than 1 day, and less than 1 week set the step interval to be multiple of 5 minutes
|
||||
// if the time range is greater than 1 week, set the step interval to be multiple of 30 mins
|
||||
if end-start >= 24*time.Hour.Milliseconds() && end-start < 7*24*time.Hour.Milliseconds() {
|
||||
|
||||
@@ -126,10 +126,17 @@ func WhichTSTableToUse(start, end int64) (int64, int64, string, string) {
|
||||
tableName = constants.SIGNOZ_TIMESERIES_v4_1DAY_TABLENAME
|
||||
localTableName = constants.SIGNOZ_TIMESERIES_v4_1DAY_LOCAL_TABLENAME
|
||||
} else {
|
||||
// adjust the start time to nearest 1 week
|
||||
start = start - (start % (time.Hour.Milliseconds() * 24 * 7))
|
||||
tableName = constants.SIGNOZ_TIMESERIES_v4_1WEEK_TABLENAME
|
||||
localTableName = constants.SIGNOZ_TIMESERIES_v4_1WEEK_LOCAL_TABLENAME
|
||||
if constants.UseMetricsPreAggregation() {
|
||||
// adjust the start time to nearest 1 week
|
||||
start = start - (start % (time.Hour.Milliseconds() * 24 * 7))
|
||||
tableName = constants.SIGNOZ_TIMESERIES_v4_1WEEK_TABLENAME
|
||||
localTableName = constants.SIGNOZ_TIMESERIES_v4_1WEEK_LOCAL_TABLENAME
|
||||
} else {
|
||||
// continue to use the 1 day table
|
||||
start = start - (start % (time.Hour.Milliseconds() * 24))
|
||||
tableName = constants.SIGNOZ_TIMESERIES_v4_1DAY_TABLENAME
|
||||
localTableName = constants.SIGNOZ_TIMESERIES_v4_1DAY_LOCAL_TABLENAME
|
||||
}
|
||||
}
|
||||
|
||||
return start, end, tableName, localTableName
|
||||
|
||||
@@ -17,7 +17,6 @@ const (
|
||||
MetricNameLabel = "__name__"
|
||||
TemporalityLabel = "__temporality__"
|
||||
AlertNameLabel = "alertname"
|
||||
TestAlertLabel = "testalert"
|
||||
NoDataLabel = "nodata"
|
||||
|
||||
// AlertStateLabel is the label name indicating the state of an alert.
|
||||
|
||||
@@ -20,6 +20,7 @@ type aggExprRewriter struct {
|
||||
fullTextColumn *telemetrytypes.TelemetryFieldKey
|
||||
fieldMapper qbtypes.FieldMapper
|
||||
conditionBuilder qbtypes.ConditionBuilder
|
||||
jsonBodyPrefix string
|
||||
jsonKeyToKey qbtypes.JsonKeyToFieldFunc
|
||||
}
|
||||
|
||||
@@ -30,6 +31,7 @@ func NewAggExprRewriter(
|
||||
fullTextColumn *telemetrytypes.TelemetryFieldKey,
|
||||
fieldMapper qbtypes.FieldMapper,
|
||||
conditionBuilder qbtypes.ConditionBuilder,
|
||||
jsonBodyPrefix string,
|
||||
jsonKeyToKey qbtypes.JsonKeyToFieldFunc,
|
||||
) *aggExprRewriter {
|
||||
set := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/querybuilder/agg_rewrite")
|
||||
@@ -39,6 +41,7 @@ func NewAggExprRewriter(
|
||||
fullTextColumn: fullTextColumn,
|
||||
fieldMapper: fieldMapper,
|
||||
conditionBuilder: conditionBuilder,
|
||||
jsonBodyPrefix: jsonBodyPrefix,
|
||||
jsonKeyToKey: jsonKeyToKey,
|
||||
}
|
||||
}
|
||||
@@ -78,6 +81,7 @@ func (r *aggExprRewriter) Rewrite(
|
||||
r.fullTextColumn,
|
||||
r.fieldMapper,
|
||||
r.conditionBuilder,
|
||||
r.jsonBodyPrefix,
|
||||
r.jsonKeyToKey,
|
||||
)
|
||||
// Rewrite the first select item (our expression)
|
||||
@@ -125,6 +129,7 @@ type exprVisitor struct {
|
||||
fullTextColumn *telemetrytypes.TelemetryFieldKey
|
||||
fieldMapper qbtypes.FieldMapper
|
||||
conditionBuilder qbtypes.ConditionBuilder
|
||||
jsonBodyPrefix string
|
||||
jsonKeyToKey qbtypes.JsonKeyToFieldFunc
|
||||
Modified bool
|
||||
chArgs []any
|
||||
@@ -137,6 +142,7 @@ func newExprVisitor(
|
||||
fullTextColumn *telemetrytypes.TelemetryFieldKey,
|
||||
fieldMapper qbtypes.FieldMapper,
|
||||
conditionBuilder qbtypes.ConditionBuilder,
|
||||
jsonBodyPrefix string,
|
||||
jsonKeyToKey qbtypes.JsonKeyToFieldFunc,
|
||||
) *exprVisitor {
|
||||
return &exprVisitor{
|
||||
@@ -145,6 +151,7 @@ func newExprVisitor(
|
||||
fullTextColumn: fullTextColumn,
|
||||
fieldMapper: fieldMapper,
|
||||
conditionBuilder: conditionBuilder,
|
||||
jsonBodyPrefix: jsonBodyPrefix,
|
||||
jsonKeyToKey: jsonKeyToKey,
|
||||
}
|
||||
}
|
||||
@@ -183,7 +190,7 @@ func (v *exprVisitor) VisitFunctionExpr(fn *chparser.FunctionExpr) error {
|
||||
if aggFunc.FuncCombinator {
|
||||
// Map the predicate (last argument)
|
||||
origPred := args[len(args)-1].String()
|
||||
whereClause, err := PrepareWhereClause(
|
||||
whereClause, err := PrepareWhereClause(
|
||||
origPred,
|
||||
FilterExprVisitorOpts{
|
||||
Logger: v.logger,
|
||||
@@ -192,7 +199,7 @@ func (v *exprVisitor) VisitFunctionExpr(fn *chparser.FunctionExpr) error {
|
||||
ConditionBuilder: v.conditionBuilder,
|
||||
FullTextColumn: v.fullTextColumn,
|
||||
JsonKeyToKey: v.jsonKeyToKey,
|
||||
}, 0, 0,
|
||||
}, 0, 0,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -212,7 +219,7 @@ func (v *exprVisitor) VisitFunctionExpr(fn *chparser.FunctionExpr) error {
|
||||
for i := 0; i < len(args)-1; i++ {
|
||||
origVal := args[i].String()
|
||||
fieldKey := telemetrytypes.GetFieldKeyFromKeyText(origVal)
|
||||
expr, exprArgs, err := CollisionHandledFinalExpr(context.Background(), &fieldKey, v.fieldMapper, v.conditionBuilder, v.fieldKeys, dataType, v.jsonKeyToKey)
|
||||
expr, exprArgs, err := CollisionHandledFinalExpr(context.Background(), &fieldKey, v.fieldMapper, v.conditionBuilder, v.fieldKeys, dataType, v.jsonBodyPrefix, v.jsonKeyToKey)
|
||||
if err != nil {
|
||||
return errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "failed to get table field name for %q", origVal)
|
||||
}
|
||||
@@ -230,7 +237,7 @@ func (v *exprVisitor) VisitFunctionExpr(fn *chparser.FunctionExpr) error {
|
||||
for i, arg := range args {
|
||||
orig := arg.String()
|
||||
fieldKey := telemetrytypes.GetFieldKeyFromKeyText(orig)
|
||||
expr, exprArgs, err := CollisionHandledFinalExpr(context.Background(), &fieldKey, v.fieldMapper, v.conditionBuilder, v.fieldKeys, dataType, v.jsonKeyToKey)
|
||||
expr, exprArgs, err := CollisionHandledFinalExpr(context.Background(), &fieldKey, v.fieldMapper, v.conditionBuilder, v.fieldKeys, dataType, v.jsonBodyPrefix, v.jsonKeyToKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ func CollisionHandledFinalExpr(
|
||||
cb qbtypes.ConditionBuilder,
|
||||
keys map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
requiredDataType telemetrytypes.FieldDataType,
|
||||
jsonBodyPrefix string,
|
||||
jsonKeyToKey qbtypes.JsonKeyToFieldFunc,
|
||||
) (string, []any, error) {
|
||||
|
||||
@@ -44,7 +45,7 @@ func CollisionHandledFinalExpr(
|
||||
|
||||
addCondition := func(key *telemetrytypes.TelemetryFieldKey) error {
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
condition, err := cb.ConditionFor(ctx, key, qbtypes.FilterOperatorExists, nil, sb, 0, 0)
|
||||
condition, err := cb.ConditionFor(ctx, key, qbtypes.FilterOperatorExists, nil, sb, 0, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -57,8 +58,8 @@ func CollisionHandledFinalExpr(
|
||||
return nil
|
||||
}
|
||||
|
||||
colName, fieldForErr := fm.FieldFor(ctx, field)
|
||||
if errors.Is(fieldForErr, qbtypes.ErrColumnNotFound) {
|
||||
colName, err := fm.FieldFor(ctx, field)
|
||||
if errors.Is(err, qbtypes.ErrColumnNotFound) {
|
||||
// the key didn't have the right context to be added to the query
|
||||
// we try to use the context we know of
|
||||
keysForField := keys[field.Name]
|
||||
@@ -81,10 +82,10 @@ func CollisionHandledFinalExpr(
|
||||
correction, found := telemetrytypes.SuggestCorrection(field.Name, maps.Keys(keys))
|
||||
if found {
|
||||
// we found a close match, in the error message send the suggestion
|
||||
return "", nil, errors.WithAdditionalf(fieldForErr, "%s", correction)
|
||||
return "", nil, errors.Wrap(err, errors.TypeInvalidInput, errors.CodeInvalidInput, correction)
|
||||
} else {
|
||||
// not even a close match, return an error
|
||||
return "", nil, errors.WithAdditionalf(fieldForErr, "field `%s` not found", field.Name)
|
||||
return "", nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "field `%s` not found", field.Name)
|
||||
}
|
||||
} else {
|
||||
for _, key := range keysForField {
|
||||
@@ -103,11 +104,10 @@ func CollisionHandledFinalExpr(
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// first if condition covers the older tests and second if condition covers the array conditions
|
||||
if !BodyJSONQueryEnabled && field.FieldContext == telemetrytypes.FieldContextBody && jsonKeyToKey != nil {
|
||||
if strings.HasPrefix(field.Name, jsonBodyPrefix) && jsonBodyPrefix != "" && jsonKeyToKey != nil {
|
||||
// TODO(nitya): enable group by on body column?
|
||||
return "", nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "Group by/Aggregation isn't available for the body column")
|
||||
} else if strings.Contains(field.Name, telemetrytypes.ArraySep) || strings.Contains(field.Name, telemetrytypes.ArrayAnyIndex) {
|
||||
return "", nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "Group by/Aggregation isn't available for the Array Paths: %s", field.Name)
|
||||
// colName, _ = jsonKeyToKey(context.Background(), field, qbtypes.FilterOperatorUnknown, dummyValue)
|
||||
} else {
|
||||
colName, _ = DataTypeCollisionHandledFieldName(field, dummyValue, colName, qbtypes.FilterOperatorUnknown)
|
||||
}
|
||||
@@ -204,7 +204,7 @@ func DataTypeCollisionHandledFieldName(key *telemetrytypes.TelemetryFieldKey, va
|
||||
// While we expect user not to send the mixed data types, it inevitably happens
|
||||
// So we handle the data type collisions here
|
||||
switch key.FieldDataType {
|
||||
case telemetrytypes.FieldDataTypeString, telemetrytypes.FieldDataTypeArrayString:
|
||||
case telemetrytypes.FieldDataTypeString:
|
||||
switch v := value.(type) {
|
||||
case float64:
|
||||
// try to convert the string value to to number
|
||||
@@ -220,12 +220,7 @@ func DataTypeCollisionHandledFieldName(key *telemetrytypes.TelemetryFieldKey, va
|
||||
value = fmt.Sprintf("%t", v)
|
||||
}
|
||||
|
||||
case telemetrytypes.FieldDataTypeInt64,
|
||||
telemetrytypes.FieldDataTypeArrayInt64,
|
||||
telemetrytypes.FieldDataTypeNumber,
|
||||
telemetrytypes.FieldDataTypeArrayNumber,
|
||||
telemetrytypes.FieldDataTypeFloat64,
|
||||
telemetrytypes.FieldDataTypeArrayFloat64:
|
||||
case telemetrytypes.FieldDataTypeFloat64, telemetrytypes.FieldDataTypeInt64, telemetrytypes.FieldDataTypeNumber:
|
||||
switch v := value.(type) {
|
||||
// why? ; CH returns an error for a simple check
|
||||
// attributes_number['http.status_code'] = 200 but not for attributes_number['http.status_code'] >= 200
|
||||
@@ -263,8 +258,7 @@ func DataTypeCollisionHandledFieldName(key *telemetrytypes.TelemetryFieldKey, va
|
||||
}
|
||||
}
|
||||
|
||||
case telemetrytypes.FieldDataTypeBool,
|
||||
telemetrytypes.FieldDataTypeArrayBool:
|
||||
case telemetrytypes.FieldDataTypeBool:
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
tblFieldName = castString(tblFieldName)
|
||||
|
||||
@@ -43,6 +43,7 @@ type resourceFilterStatementBuilder[T any] struct {
|
||||
signal telemetrytypes.Signal
|
||||
|
||||
fullTextColumn *telemetrytypes.TelemetryFieldKey
|
||||
jsonBodyPrefix string
|
||||
jsonKeyToKey qbtypes.JsonKeyToFieldFunc
|
||||
}
|
||||
|
||||
@@ -75,6 +76,7 @@ func NewLogResourceFilterStatementBuilder(
|
||||
conditionBuilder qbtypes.ConditionBuilder,
|
||||
metadataStore telemetrytypes.MetadataStore,
|
||||
fullTextColumn *telemetrytypes.TelemetryFieldKey,
|
||||
jsonBodyPrefix string,
|
||||
jsonKeyToKey qbtypes.JsonKeyToFieldFunc,
|
||||
) *resourceFilterStatementBuilder[qbtypes.LogAggregation] {
|
||||
set := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/querybuilder/resourcefilter")
|
||||
@@ -85,6 +87,7 @@ func NewLogResourceFilterStatementBuilder(
|
||||
metadataStore: metadataStore,
|
||||
signal: telemetrytypes.SignalLogs,
|
||||
fullTextColumn: fullTextColumn,
|
||||
jsonBodyPrefix: jsonBodyPrefix,
|
||||
jsonKeyToKey: jsonKeyToKey,
|
||||
}
|
||||
}
|
||||
@@ -97,18 +100,12 @@ func (b *resourceFilterStatementBuilder[T]) getKeySelectors(query qbtypes.QueryB
|
||||
keySelectors = append(keySelectors, whereClauseSelectors...)
|
||||
}
|
||||
|
||||
// exclude out the body related key selectors
|
||||
filteredKeySelectors := []*telemetrytypes.FieldKeySelector{}
|
||||
for idx := range keySelectors {
|
||||
if keySelectors[idx].FieldContext == telemetrytypes.FieldContextBody {
|
||||
continue
|
||||
}
|
||||
keySelectors[idx].Signal = b.signal
|
||||
keySelectors[idx].SelectorMatchType = telemetrytypes.FieldSelectorMatchTypeExact
|
||||
filteredKeySelectors = append(filteredKeySelectors, keySelectors[idx])
|
||||
}
|
||||
|
||||
return filteredKeySelectors
|
||||
return keySelectors
|
||||
}
|
||||
|
||||
// Build builds a SQL query based on the given parameters
|
||||
@@ -171,7 +168,7 @@ func (b *resourceFilterStatementBuilder[T]) addConditions(
|
||||
// there is no need for "key" not found error for resource filtering
|
||||
IgnoreNotFoundKeys: true,
|
||||
Variables: variables,
|
||||
}, start, end)
|
||||
}, start, end)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -16,13 +16,14 @@ type provider struct {
|
||||
ruleStore ruletypes.RuleStore
|
||||
}
|
||||
|
||||
func NewFactory(sqlstore sqlstore.SQLStore, queryParser queryparser.QueryParser) factory.ProviderFactory[ruler.Ruler, ruler.Config] {
|
||||
func NewFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[ruler.Ruler, ruler.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, settings factory.ProviderSettings, config ruler.Config) (ruler.Ruler, error) {
|
||||
return New(ctx, settings, config, sqlstore, queryParser)
|
||||
return New(ctx, settings, config, sqlstore)
|
||||
})
|
||||
}
|
||||
|
||||
func New(ctx context.Context, settings factory.ProviderSettings, config ruler.Config, sqlstore sqlstore.SQLStore, queryParser queryparser.QueryParser) (ruler.Ruler, error) {
|
||||
func New(ctx context.Context, settings factory.ProviderSettings, config ruler.Config, sqlstore sqlstore.SQLStore) (ruler.Ruler, error) {
|
||||
queryParser := queryparser.New(settings)
|
||||
return &provider{ruleStore: sqlrulestore.NewRuleStore(sqlstore, queryParser, settings)}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ func (df *DeprecatedFlags) RegisterFlags(cmd *cobra.Command) {
|
||||
_ = cmd.Flags().MarkDeprecated("flux-interval", "use SIGNOZ_QUERIER_FLUX__INTERVAL instead")
|
||||
_ = cmd.Flags().MarkDeprecated("flux-interval-for-trace-detail", "use SIGNOZ_QUERIER_FLUX__INTERVAL instead")
|
||||
_ = cmd.Flags().MarkDeprecated("cluster", "use SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_CLUSTER instead")
|
||||
_ = cmd.Flags().MarkDeprecated("prefer-span-metrics", "use SIGNOZ_FLAGGER_CONFIG_BOOLEAN_USE__SPAN__METRICS instead")
|
||||
_ = cmd.Flags().MarkDeprecated("prefer-span-metrics", "use USE_SPAN_METRICS instead")
|
||||
_ = cmd.Flags().MarkDeprecated("gateway-url", "use SIGNOZ_GATEWAY_URL instead")
|
||||
}
|
||||
|
||||
@@ -332,19 +332,7 @@ func mergeAndEnsureBackwardCompatibility(ctx context.Context, logger *slog.Logge
|
||||
}
|
||||
|
||||
if deprecatedFlags.PreferSpanMetrics {
|
||||
logger.WarnContext(ctx, "[Deprecated] flag --prefer-span-metrics is deprecated and scheduled for removal. Please use SIGNOZ_FLAGGER_CONFIG_BOOLEAN_USE__SPAN__METRICS instead.")
|
||||
if config.Flagger.Config.Boolean == nil {
|
||||
config.Flagger.Config.Boolean = make(map[string]bool)
|
||||
}
|
||||
config.Flagger.Config.Boolean[flagger.FeatureUseSpanMetrics.String()] = deprecatedFlags.PreferSpanMetrics
|
||||
}
|
||||
|
||||
if os.Getenv("USE_SPAN_METRICS") != "" {
|
||||
logger.WarnContext(ctx, "[Deprecated] env USE_SPAN_METRICS is deprecated and scheduled for removal. Please use SIGNOZ_FLAGGER_CONFIG_BOOLEAN_USE__SPAN__METRICS instead.")
|
||||
if config.Flagger.Config.Boolean == nil {
|
||||
config.Flagger.Config.Boolean = make(map[string]bool)
|
||||
}
|
||||
config.Flagger.Config.Boolean[flagger.FeatureUseSpanMetrics.String()] = os.Getenv("USE_SPAN_METRICS") == "true"
|
||||
logger.WarnContext(ctx, "[Deprecated] flag --prefer-span-metrics is deprecated and scheduled for removal. Please use USE_SPAN_METRICS instead.")
|
||||
}
|
||||
|
||||
if deprecatedFlags.GatewayUrl != "" {
|
||||
@@ -361,22 +349,6 @@ func mergeAndEnsureBackwardCompatibility(ctx context.Context, logger *slog.Logge
|
||||
logger.WarnContext(ctx, "[Deprecated] env SIGNOZ_JWT_SECRET is deprecated and scheduled for removal. Please use SIGNOZ_TOKENIZER_JWT_SECRET instead.")
|
||||
config.Tokenizer.JWT.Secret = os.Getenv("SIGNOZ_JWT_SECRET")
|
||||
}
|
||||
|
||||
if os.Getenv("KAFKA_SPAN_EVAL") != "" {
|
||||
logger.WarnContext(ctx, "[Deprecated] env KAFKA_SPAN_EVAL is deprecated and scheduled for removal. Please use SIGNOZ_FLAGGER_CONFIG_BOOLEAN_KAFKA__SPAN__EVAL instead.")
|
||||
if config.Flagger.Config.Boolean == nil {
|
||||
config.Flagger.Config.Boolean = make(map[string]bool)
|
||||
}
|
||||
config.Flagger.Config.Boolean[flagger.FeatureKafkaSpanEval.String()] = os.Getenv("KAFKA_SPAN_EVAL") == "true"
|
||||
}
|
||||
|
||||
if os.Getenv("INTERPOLATION_ENABLED") != "" {
|
||||
logger.WarnContext(ctx, "[Deprecated] env INTERPOLATION_ENABLED is deprecated and scheduled for removal. Please use SIGNOZ_FLAGGER_CONFIG_BOOLEAN_INTERPOLATION__ENABLED instead.")
|
||||
if config.Flagger.Config.Boolean == nil {
|
||||
config.Flagger.Config.Boolean = make(map[string]bool)
|
||||
}
|
||||
config.Flagger.Config.Boolean[flagger.FeatureInterpolationEnabled.String()] = os.Getenv("INTERPOLATION_ENABLED") == "true"
|
||||
}
|
||||
}
|
||||
|
||||
func (config Config) Collect(_ context.Context, _ valuer.UUID) (map[string]any, error) {
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation"
|
||||
"github.com/SigNoz/signoz/pkg/modules/authdomain"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/preference"
|
||||
"github.com/SigNoz/signoz/pkg/modules/promote"
|
||||
@@ -46,7 +45,6 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
|
||||
struct{ flagger.Handler }{},
|
||||
struct{ dashboard.Module }{},
|
||||
struct{ dashboard.Handler }{},
|
||||
struct{ metricsexplorer.Handler }{},
|
||||
).New(ctx, instrumentation.ToProviderSettings(), apiserver.Config{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -34,7 +34,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/prometheus/clickhouseprometheus"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/querier/signozquerier"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
"github.com/SigNoz/signoz/pkg/ruler"
|
||||
"github.com/SigNoz/signoz/pkg/ruler/signozruler"
|
||||
"github.com/SigNoz/signoz/pkg/sharder"
|
||||
@@ -198,9 +197,9 @@ func NewAlertmanagerProviderFactories(sqlstore sqlstore.SQLStore, orgGetter orga
|
||||
)
|
||||
}
|
||||
|
||||
func NewRulerProviderFactories(sqlstore sqlstore.SQLStore, queryParser queryparser.QueryParser) factory.NamedMap[factory.ProviderFactory[ruler.Ruler, ruler.Config]] {
|
||||
func NewRulerProviderFactories(sqlstore sqlstore.SQLStore) factory.NamedMap[factory.ProviderFactory[ruler.Ruler, ruler.Config]] {
|
||||
return factory.MustNewNamedMap(
|
||||
signozruler.NewFactory(sqlstore, queryParser),
|
||||
signozruler.NewFactory(sqlstore),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -225,9 +224,9 @@ func NewStatsReporterProviderFactories(telemetryStore telemetrystore.TelemetrySt
|
||||
)
|
||||
}
|
||||
|
||||
func NewQuerierProviderFactories(telemetryStore telemetrystore.TelemetryStore, prometheus prometheus.Prometheus, cache cache.Cache, flagger flagger.Flagger) factory.NamedMap[factory.ProviderFactory[querier.Querier, querier.Config]] {
|
||||
func NewQuerierProviderFactories(telemetryStore telemetrystore.TelemetryStore, prometheus prometheus.Prometheus, cache cache.Cache) factory.NamedMap[factory.ProviderFactory[querier.Querier, querier.Config]] {
|
||||
return factory.MustNewNamedMap(
|
||||
signozquerier.NewFactory(telemetryStore, prometheus, cache, flagger),
|
||||
signozquerier.NewFactory(telemetryStore, prometheus, cache),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -246,7 +245,6 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
|
||||
handlers.FlaggerHandler,
|
||||
modules.Dashboard,
|
||||
handlers.Dashboard,
|
||||
handlers.MetricsExplorer,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
||||
"github.com/SigNoz/signoz/pkg/queryparser"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema/sqlschematest"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
@@ -62,8 +61,7 @@ func TestNewProviderFactories(t *testing.T) {
|
||||
})
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
queryParser := queryparser.New(instrumentationtest.New().ToProviderSettings())
|
||||
NewRulerProviderFactories(sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherEqual), queryParser)
|
||||
NewRulerProviderFactories(sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherEqual))
|
||||
})
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
|
||||
@@ -149,20 +149,6 @@ func New(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize flagger from the available flagger provider factories
|
||||
flaggerRegistry := flagger.MustNewRegistry()
|
||||
flaggerProviderFactories := NewFlaggerProviderFactories(flaggerRegistry)
|
||||
flagger, err := flagger.New(
|
||||
ctx,
|
||||
providerSettings,
|
||||
config.Flagger,
|
||||
flaggerRegistry,
|
||||
flaggerProviderFactories.GetInOrder()...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize web from the available web provider factories
|
||||
web, err := factory.NewProviderFromNamedMap(
|
||||
ctx,
|
||||
@@ -216,7 +202,7 @@ func New(
|
||||
ctx,
|
||||
providerSettings,
|
||||
config.Querier,
|
||||
NewQuerierProviderFactories(telemetrystore, prometheus, cache, flagger),
|
||||
NewQuerierProviderFactories(telemetrystore, prometheus, cache),
|
||||
config.Querier.Provider(),
|
||||
)
|
||||
if err != nil {
|
||||
@@ -311,15 +297,12 @@ func New(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize query parser
|
||||
queryParser := queryparser.New(providerSettings)
|
||||
|
||||
// Initialize ruler from the available ruler provider factories
|
||||
ruler, err := factory.NewProviderFromNamedMap(
|
||||
ctx,
|
||||
providerSettings,
|
||||
config.Ruler,
|
||||
NewRulerProviderFactories(sqlstore, queryParser),
|
||||
NewRulerProviderFactories(sqlstore),
|
||||
"signoz",
|
||||
)
|
||||
if err != nil {
|
||||
@@ -336,6 +319,9 @@ func New(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize query parser
|
||||
queryParser := queryparser.New(providerSettings)
|
||||
|
||||
// Initialize authns
|
||||
store := sqlauthnstore.NewStore(sqlstore)
|
||||
authNs, err := authNsCallback(ctx, providerSettings, store, licensing)
|
||||
@@ -376,6 +362,20 @@ func New(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize flagger from the available flagger provider factories
|
||||
flaggerRegistry := flagger.MustNewRegistry()
|
||||
flaggerProviderFactories := NewFlaggerProviderFactories(flaggerRegistry)
|
||||
flagger, err := flagger.New(
|
||||
ctx,
|
||||
providerSettings,
|
||||
config.Flagger,
|
||||
flaggerRegistry,
|
||||
flaggerProviderFactories.GetInOrder()...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize all modules
|
||||
roleModule := implrole.NewModule(implrole.NewStore(sqlstore), authz, nil)
|
||||
dashboardModule := dashboardModuleCallback(sqlstore, providerSettings, analytics, orgGetter, roleModule, queryParser, querier, licensing)
|
||||
|
||||
@@ -16,12 +16,11 @@ import (
|
||||
)
|
||||
|
||||
type conditionBuilder struct {
|
||||
fm qbtypes.FieldMapper
|
||||
metadataStore telemetrytypes.MetadataStore
|
||||
fm qbtypes.FieldMapper
|
||||
}
|
||||
|
||||
func NewConditionBuilder(fm qbtypes.FieldMapper, metadataStore telemetrytypes.MetadataStore) *conditionBuilder {
|
||||
return &conditionBuilder{fm: fm, metadataStore: metadataStore}
|
||||
func NewConditionBuilder(fm qbtypes.FieldMapper) *conditionBuilder {
|
||||
return &conditionBuilder{fm: fm}
|
||||
}
|
||||
|
||||
func (c *conditionBuilder) conditionFor(
|
||||
@@ -31,23 +30,22 @@ func (c *conditionBuilder) conditionFor(
|
||||
value any,
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) (string, error) {
|
||||
|
||||
switch operator {
|
||||
case qbtypes.FilterOperatorContains,
|
||||
qbtypes.FilterOperatorNotContains,
|
||||
qbtypes.FilterOperatorILike,
|
||||
qbtypes.FilterOperatorNotILike,
|
||||
qbtypes.FilterOperatorLike,
|
||||
qbtypes.FilterOperatorNotLike:
|
||||
value = querybuilder.FormatValueForContains(value)
|
||||
}
|
||||
|
||||
column, err := c.fm.ColumnFor(ctx, key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if column.IsJSONColumn() && querybuilder.BodyJSONQueryEnabled {
|
||||
cond, err := c.buildJSONCondition(ctx, key, operator, value, sb)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return cond, nil
|
||||
}
|
||||
|
||||
if operator.IsStringSearchOperator() {
|
||||
value = querybuilder.FormatValueForContains(value)
|
||||
}
|
||||
|
||||
tblFieldName, err := c.fm.FieldFor(ctx, key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -165,7 +163,9 @@ func (c *conditionBuilder) conditionFor(
|
||||
// in the UI based query builder, `exists` and `not exists` are used for
|
||||
// key membership checks, so depending on the column type, the condition changes
|
||||
case qbtypes.FilterOperatorExists, qbtypes.FilterOperatorNotExists:
|
||||
if key.FieldContext == telemetrytypes.FieldContextBody && !querybuilder.BodyJSONQueryEnabled {
|
||||
|
||||
// Check if this is a body JSON search - by FieldContext
|
||||
if key.FieldContext == telemetrytypes.FieldContextBody {
|
||||
if operator == qbtypes.FilterOperatorExists {
|
||||
return GetBodyJSONKeyForExists(ctx, key, operator, value), nil
|
||||
} else {
|
||||
@@ -247,7 +247,7 @@ func (c *conditionBuilder) ConditionFor(
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !(key.FieldContext == telemetrytypes.FieldContextBody && querybuilder.BodyJSONQueryEnabled) && operator.AddDefaultExistsFilter() {
|
||||
if operator.AddDefaultExistsFilter() {
|
||||
// skip adding exists filter for intrinsic fields
|
||||
// with an exception for body json search
|
||||
field, _ := c.fm.FieldFor(ctx, key)
|
||||
|
||||
@@ -373,8 +373,7 @@ func TestConditionFor(t *testing.T) {
|
||||
}
|
||||
|
||||
fm := NewFieldMapper()
|
||||
mockMetadataStore := buildTestTelemetryMetadataStore()
|
||||
conditionBuilder := NewConditionBuilder(fm, mockMetadataStore)
|
||||
conditionBuilder := NewConditionBuilder(fm)
|
||||
|
||||
for _, tc := range testCases {
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
@@ -427,8 +426,7 @@ func TestConditionForMultipleKeys(t *testing.T) {
|
||||
}
|
||||
|
||||
fm := NewFieldMapper()
|
||||
mockMetadataStore := buildTestTelemetryMetadataStore()
|
||||
conditionBuilder := NewConditionBuilder(fm, mockMetadataStore)
|
||||
conditionBuilder := NewConditionBuilder(fm)
|
||||
|
||||
for _, tc := range testCases {
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
@@ -687,8 +685,7 @@ func TestConditionForJSONBodySearch(t *testing.T) {
|
||||
}
|
||||
|
||||
fm := NewFieldMapper()
|
||||
mockMetadataStore := buildTestTelemetryMetadataStore()
|
||||
conditionBuilder := NewConditionBuilder(fm, mockMetadataStore)
|
||||
conditionBuilder := NewConditionBuilder(fm)
|
||||
|
||||
for _, tc := range testCases {
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
|
||||
@@ -2,6 +2,7 @@ package telemetrylogs
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz-otel-collector/constants"
|
||||
"github.com/SigNoz/signoz-otel-collector/exporter/jsontypeexporter"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
@@ -36,6 +37,8 @@ const (
|
||||
|
||||
BodyJSONColumnPrefix = constants.BodyJSONColumnPrefix
|
||||
BodyPromotedColumnPrefix = constants.BodyPromotedColumnPrefix
|
||||
ArraySep = jsontypeexporter.ArraySeparator
|
||||
ArrayAnyIndex = "[*]."
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -45,7 +48,8 @@ var (
|
||||
FieldContext: telemetrytypes.FieldContextLog,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
}
|
||||
IntrinsicFields = map[string]telemetrytypes.TelemetryFieldKey{
|
||||
BodyJSONStringSearchPrefix = `body.`
|
||||
IntrinsicFields = map[string]telemetrytypes.TelemetryFieldKey{
|
||||
"body": {
|
||||
Name: "body",
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
|
||||
@@ -6,9 +6,7 @@ import (
|
||||
"strings"
|
||||
|
||||
schema "github.com/SigNoz/signoz-otel-collector/cmd/signozschemamigrator/schema_migrator"
|
||||
"github.com/SigNoz/signoz-otel-collector/utils"
|
||||
"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"
|
||||
@@ -30,11 +28,6 @@ var (
|
||||
"severity_text": {Name: "severity_text", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}},
|
||||
"severity_number": {Name: "severity_number", Type: schema.ColumnTypeUInt8},
|
||||
"body": {Name: "body", Type: schema.ColumnTypeString},
|
||||
LogsV2BodyJSONColumn: {Name: LogsV2BodyJSONColumn, Type: schema.JSONColumnType{
|
||||
MaxDynamicTypes: utils.ToPointer(uint(32)),
|
||||
MaxDynamicPaths: utils.ToPointer(uint(0)),
|
||||
}},
|
||||
LogsV2BodyPromotedColumn: {Name: LogsV2BodyPromotedColumn, Type: schema.JSONColumnType{}},
|
||||
"attributes_string": {Name: "attributes_string", Type: schema.MapColumnType{
|
||||
KeyType: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString},
|
||||
ValueType: schema.ColumnTypeString,
|
||||
@@ -90,23 +83,13 @@ func (m *fieldMapper) getColumn(_ context.Context, key *telemetrytypes.Telemetry
|
||||
return logsV2Columns["attributes_bool"], nil
|
||||
}
|
||||
case telemetrytypes.FieldContextBody:
|
||||
// Body context is for JSON body fields
|
||||
// Use body_json if feature flag is enabled
|
||||
if querybuilder.BodyJSONQueryEnabled {
|
||||
return logsV2Columns[LogsV2BodyJSONColumn], nil
|
||||
}
|
||||
// Fall back to legacy body column
|
||||
// body context fields are stored in the body column
|
||||
return logsV2Columns["body"], nil
|
||||
case telemetrytypes.FieldContextLog, telemetrytypes.FieldContextUnspecified:
|
||||
col, ok := logsV2Columns[key.Name]
|
||||
if !ok {
|
||||
// check if the key has body JSON search
|
||||
if strings.HasPrefix(key.Name, telemetrytypes.BodyJSONStringSearchPrefix) {
|
||||
// Use body_json if feature flag is enabled and we have a body condition builder
|
||||
if querybuilder.BodyJSONQueryEnabled {
|
||||
return logsV2Columns[LogsV2BodyJSONColumn], nil
|
||||
}
|
||||
// Fall back to legacy body column
|
||||
// check if the key has body JSON search (backward compatibility)
|
||||
if strings.HasPrefix(key.Name, BodyJSONStringSearchPrefix) {
|
||||
return logsV2Columns["body"], nil
|
||||
}
|
||||
return nil, qbtypes.ErrColumnNotFound
|
||||
@@ -126,36 +109,20 @@ func (m *fieldMapper) FieldFor(ctx context.Context, key *telemetrytypes.Telemetr
|
||||
switch column.Type.GetType() {
|
||||
case schema.ColumnTypeEnumJSON:
|
||||
// json is only supported for resource context as of now
|
||||
switch key.FieldContext {
|
||||
case telemetrytypes.FieldContextResource:
|
||||
oldColumn := logsV2Columns["resources_string"]
|
||||
oldKeyName := fmt.Sprintf("%s['%s']", oldColumn.Name, key.Name)
|
||||
if key.FieldContext != telemetrytypes.FieldContextResource {
|
||||
return "", errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "only resource context fields are supported for json columns, got %s", key.FieldContext.String)
|
||||
}
|
||||
oldColumn := logsV2Columns["resources_string"]
|
||||
oldKeyName := fmt.Sprintf("%s['%s']", oldColumn.Name, key.Name)
|
||||
|
||||
// have to add ::string as clickHouse throws an error :- data types Variant/Dynamic are not allowed in GROUP BY
|
||||
// once clickHouse dependency is updated, we need to check if we can remove it.
|
||||
if key.Materialized {
|
||||
oldKeyName = telemetrytypes.FieldKeyToMaterializedColumnName(key)
|
||||
oldKeyNameExists := telemetrytypes.FieldKeyToMaterializedColumnNameForExists(key)
|
||||
return fmt.Sprintf("multiIf(%s.`%s` IS NOT NULL, %s.`%s`::String, %s==true, %s, NULL)", column.Name, key.Name, column.Name, key.Name, oldKeyNameExists, oldKeyName), nil
|
||||
}
|
||||
// have to add ::string as clickHouse throws an error :- data types Variant/Dynamic are not allowed in GROUP BY
|
||||
// once clickHouse dependency is updated, we need to check if we can remove it.
|
||||
if key.Materialized {
|
||||
oldKeyName = telemetrytypes.FieldKeyToMaterializedColumnName(key)
|
||||
oldKeyNameExists := telemetrytypes.FieldKeyToMaterializedColumnNameForExists(key)
|
||||
return fmt.Sprintf("multiIf(%s.`%s` IS NOT NULL, %s.`%s`::String, %s==true, %s, NULL)", column.Name, key.Name, column.Name, key.Name, oldKeyNameExists, oldKeyName), nil
|
||||
} else {
|
||||
return fmt.Sprintf("multiIf(%s.`%s` IS NOT NULL, %s.`%s`::String, mapContains(%s, '%s'), %s, NULL)", column.Name, key.Name, column.Name, key.Name, oldColumn.Name, key.Name, oldKeyName), nil
|
||||
case telemetrytypes.FieldContextBody:
|
||||
if querybuilder.BodyJSONQueryEnabled && (strings.Contains(key.Name, telemetrytypes.ArraySep) || strings.Contains(key.Name, telemetrytypes.ArrayAnyIndex)) {
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "FieldFor not supported for the Array Paths: %s", key.Name)
|
||||
}
|
||||
if key.JSONDataType == nil {
|
||||
return "", qbtypes.ErrColumnNotFound
|
||||
}
|
||||
|
||||
fieldExpr := BodyJSONColumnPrefix + fmt.Sprintf("`%s`", key.Name)
|
||||
expr := fmt.Sprintf("dynamicElement(%s, '%s')", fieldExpr, key.JSONDataType.StringValue())
|
||||
if key.Materialized {
|
||||
promotedFieldExpr := BodyPromotedColumnPrefix + fmt.Sprintf("`%s`", key.Name)
|
||||
expr = fmt.Sprintf("coalesce(%s, %s)", expr, fmt.Sprintf("dynamicElement(%s, '%s')", promotedFieldExpr, key.JSONDataType.StringValue()))
|
||||
}
|
||||
return expr, nil
|
||||
default:
|
||||
return "", errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "only resource/body context fields are supported for json columns, got %s", key.FieldContext.String)
|
||||
}
|
||||
case schema.ColumnTypeEnumLowCardinality:
|
||||
switch elementType := column.Type.(schema.LowCardinalityColumnType).ElementType; elementType.GetType() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user