Compare commits

..

3 Commits

Author SHA1 Message Date
aks07
bddd80ad9f feat: minor refactor 2026-01-08 16:01:39 +05:30
aks07
82bbc35acd feat: show badge for mulitple varient attributes 2026-01-08 08:06:50 +05:30
aks07
2aaa2a8158 fix: add unique to attribute options to avoid conflicts in matching attribute names 2026-01-06 16:35:02 +05:30
153 changed files with 2394 additions and 7469 deletions

View File

@@ -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**

View File

@@ -51,7 +51,6 @@ jobs:
- sqlite
clickhouse-version:
- 25.5.6
- 25.10.1
schema-migrator-version:
- v0.129.7
postgres-version:

View File

@@ -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"

View File

@@ -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
}

View File

@@ -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:

View File

@@ -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:

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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
}
}

View File

@@ -120,6 +120,5 @@ module.exports = {
usePrettierrc: true,
},
],
'react/jsx-props-no-spreading': 'off',
},
};

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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);
}
}
}
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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();
});
});

View File

@@ -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>

View File

@@ -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,
},
};

View File

@@ -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,

View File

@@ -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);
}
}
}

View File

@@ -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

View File

@@ -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={

View File

@@ -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) {

View File

@@ -35,11 +35,6 @@
display: flex;
align-items: center;
justify-content: center;
&.disabled-btn {
opacity: 0.5;
cursor: not-allowed;
}
}
.ant-tabs-ink-bar {

View File

@@ -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');
});
});
});
});

View File

@@ -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} />}

View File

@@ -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';

View File

@@ -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" />;

View File

@@ -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

View File

@@ -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);
}
}
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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>
);
}

View File

@@ -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;
`;

View File

@@ -52,7 +52,7 @@ export const defaultTraceSelectedColumns: TelemetryFieldKey[] = [
name: 'duration_nano',
signal: 'traces',
fieldContext: 'span',
fieldDataType: '',
fieldDataType: 'number',
},
{
name: 'http_method',

View File

@@ -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 />

View File

@@ -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;
};
};

View File

@@ -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) {

View File

@@ -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;
};

View File

@@ -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({

View File

@@ -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);
});
});

View File

@@ -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,
});

View File

@@ -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(),
});

View File

@@ -1,9 +0,0 @@
export interface GlobalConfigData {
external_url: string;
ingestion_url: string;
}
export interface GlobalConfigDataProps {
status: string;
data: GlobalConfigData;
}

View File

@@ -208,7 +208,6 @@ export enum QueryFunctionsTypes {
MEDIAN_5 = 'median5',
MEDIAN_7 = 'median7',
TIME_SHIFT = 'timeShift',
FILL_ZERO = 'fillZero',
}
export type PanelTypeKeys =

View File

@@ -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,
);
});
});

View File

@@ -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
View File

@@ -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

View File

@@ -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)
})
}()

View File

@@ -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

View File

@@ -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)
}))

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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)
}

View File

@@ -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,

View File

@@ -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
}
}
}
}
}

View File

@@ -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) {

View File

@@ -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,
},
}

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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")
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"))

View File

@@ -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() {

View File

@@ -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() {

View File

@@ -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

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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

View File

@@ -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,
),
)
}

View File

@@ -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() {

View File

@@ -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)

View File

@@ -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)

View File

@@ -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()

View File

@@ -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,

View File

@@ -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