Compare commits

...

5 Commits

Author SHA1 Message Date
Vinicius Lourenço
7646aabb2b fix(member-settings): sidenav button not redirecting to correct invite members modal (#11871)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
2026-06-26 17:18:19 +00:00
Vinicius Lourenço
18851afb6d refactor(onboarding): drop old page (#11860) 2026-06-26 17:16:24 +00:00
Srikanth Chekuri
d5221a6ff3 feat(metrics): add per-metric label-reduction rules (#11849)
* chore(metric-reduction): scaffold metric volume control API (types, routes, stubs)

* chore: generate spec

* chore: fix required and generate frontend types

* chore(metric-reduction): add module implementation (#11857)
2026-06-26 17:16:04 +00:00
Vikrant Gupta
19712c3579 feat(authdomain): support custom roles in SSO group mapping (#11858)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* feat(authdomain): support custom roles in SSO group mapping

Role mappings now reference SigNoz roles by name (custom or managed)
instead of the legacy ADMIN/EDITOR/VIEWER enum. Legacy values sent by a
client are normalized to their managed names (signoz-admin/editor/viewer),
and a migration normalizes existing stored mappings.

- normalize legacy role names on unmarshal; resolve all matched roles on
  SSO callback (union of matched groups, else default, else viewer)
- validate referenced roles exist on auth domain create/update
- block deleting a role referenced by any auth domain mapping, naming the
  referencing domains (OnBeforeRoleDelete now also passes the role name)
- migration 096 to rewrite legacy names in existing auth_domain mappings

* refactor(sqlmigration): decode SSO role mapping into a typed struct

Decode the roleMapping object into a small frozen struct instead of
poking at json.RawMessage per field. The top-level config stays a raw
map so the other config fields are preserved untouched.

* fix(authdomain): validate SSO role attribute claim against existing roles

When the role mapping uses the IDP role attribute, the claimed role is
assigned only if it exists in the org (resolved case-insensitively, with
legacy ADMIN/EDITOR/VIEWER mapped to their managed names); otherwise the
mapping falls through to group mappings and the default role. This lets
custom roles be assigned via the attribute and avoids failing login on an
unknown claim.

Also normalize legacy role names case-insensitively and fold the
single-use managed-role lookup into NormalizeRoleName.
2026-06-26 14:26:43 +00:00
ElyesLaaribi
e31683be11 fix(metrics-explorer): resolve pagination overlap in table (#11812)
* fix: resolve pagination overlap in metrics explorer table

* fix: add padding-bottom to metrics table container per review feedback
2026-06-26 13:59:04 +00:00
1062 changed files with 6488 additions and 54073 deletions

View File

@@ -56,17 +56,6 @@ jobs:
PRIMUS_REF: main
JS_SRC: frontend
JS_PKG_MANAGER: pnpm
languages:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
runs-on: ubuntu-latest
steps:
- name: self-checkout
uses: actions/checkout@v4
- name: run
run: bash frontend/scripts/validate-md-languages.sh
openapi:
if: |
github.event_name == 'merge_group' ||

View File

@@ -29,6 +29,8 @@ import (
"github.com/SigNoz/signoz/pkg/modules/cloudintegration/implcloudintegration"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/metricreductionrule"
"github.com/SigNoz/signoz/pkg/modules/metricreductionrule/implmetricreductionrule"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/retention"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
@@ -119,6 +121,9 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
func(_ sqlstore.SQLStore, _ dashboard.Module, _ global.Global, _ zeus.Zeus, _ gateway.Gateway, _ licensing.Licensing, _ serviceaccount.Module, _ cloudintegration.Config) (cloudintegration.Module, error) {
return implcloudintegration.NewModule(), nil
},
func(_ sqlstore.SQLStore, _ telemetrystore.TelemetryStore, _ dashboard.Module, _ queryparser.QueryParser, _ licensing.Licensing, _ flagger.Flagger, _ telemetrytypes.MetadataStore, _ factory.ProviderSettings, _ int) metricreductionrule.Module {
return implmetricreductionrule.NewModule()
},
func(c cache.Cache, am alertmanager.Alertmanager, ss sqlstore.SQLStore, ts telemetrystore.TelemetryStore, ms telemetrytypes.MetadataStore, p prometheus.Prometheus, og organization.Getter, rsh rulestatehistory.Module, q querier.Querier, qp queryparser.QueryParser) factory.NamedMap[factory.ProviderFactory[ruler.Ruler, ruler.Config]] {
return factory.MustNewNamedMap(signozruler.NewFactory(c, am, ss, ts, ms, p, og, rsh, q, qp, nil, nil))
},

View File

@@ -24,6 +24,7 @@ import (
"github.com/SigNoz/signoz/ee/modules/cloudintegration/implcloudintegration"
"github.com/SigNoz/signoz/ee/modules/cloudintegration/implcloudintegration/implcloudprovider"
"github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard"
eeimplmetricreductionrule "github.com/SigNoz/signoz/ee/modules/metricreductionrule/implmetricreductionrule"
eequerier "github.com/SigNoz/signoz/ee/querier"
enterpriseapp "github.com/SigNoz/signoz/ee/query-service/app"
eerules "github.com/SigNoz/signoz/ee/query-service/rules"
@@ -46,6 +47,7 @@ import (
pkgcloudintegration "github.com/SigNoz/signoz/pkg/modules/cloudintegration/implcloudintegration"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/metricreductionrule"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/retention"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
@@ -182,6 +184,9 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
return implcloudintegration.NewModule(pkgcloudintegration.NewStore(sqlStore), dashboardModule, global, zeus, gateway, licensing, serviceAccount, cloudProvidersMap, config)
},
func(sqlStore sqlstore.SQLStore, ts telemetrystore.TelemetryStore, dashboardModule dashboard.Module, queryParser queryparser.QueryParser, lic licensing.Licensing, flgr pkgflagger.Flagger, ms telemetrytypes.MetadataStore, ps factory.ProviderSettings, threads int) metricreductionrule.Module {
return eeimplmetricreductionrule.NewModule(sqlStore, ts, dashboardModule, queryParser, lic, flgr, ms, ps, threads)
},
func(c cache.Cache, am alertmanager.Alertmanager, ss sqlstore.SQLStore, ts telemetrystore.TelemetryStore, ms telemetrytypes.MetadataStore, p prometheus.Prometheus, og organization.Getter, rsh rulestatehistory.Module, q querier.Querier, qp queryparser.QueryParser) factory.NamedMap[factory.ProviderFactory[ruler.Ruler, ruler.Config]] {
return factory.MustNewNamedMap(signozruler.NewFactory(c, am, ss, ts, ms, p, og, rsh, q, qp, eerules.PrepareTaskFunc, eerules.TestNotification))
},

View File

@@ -5249,6 +5249,221 @@ components:
required:
- rules
type: object
MetricreductionruletypesAffectedAsset:
properties:
id:
type: string
impactedLabels:
items:
type: string
nullable: true
type: array
name:
type: string
type:
$ref: '#/components/schemas/MetricreductionruletypesAssetType'
widget:
$ref: '#/components/schemas/MetricreductionruletypesAffectedWidget'
required:
- type
- id
- name
- impactedLabels
type: object
MetricreductionruletypesAffectedWidget:
properties:
id:
type: string
name:
type: string
required:
- id
- name
type: object
MetricreductionruletypesAssetType:
enum:
- dashboard
- alert_rule
type: string
MetricreductionruletypesGettableReductionRule:
properties:
active:
type: boolean
createdAt:
format: date-time
type: string
createdBy:
type: string
effectiveFrom:
format: date-time
type: string
id:
type: string
ingestedSeries:
minimum: 0
type: integer
labels:
items:
type: string
nullable: true
type: array
matchType:
$ref: '#/components/schemas/MetricreductionruletypesMatchType'
metricName:
type: string
reductionPercent:
format: double
type: number
retainedSeries:
minimum: 0
type: integer
updatedAt:
format: date-time
type: string
updatedBy:
type: string
required:
- id
- metricName
- matchType
- labels
- effectiveFrom
- active
- ingestedSeries
- retainedSeries
- reductionPercent
type: object
MetricreductionruletypesGettableReductionRulePreview:
properties:
affectedAssets:
items:
$ref: '#/components/schemas/MetricreductionruletypesAffectedAsset'
nullable: true
type: array
currentRetainedSeries:
minimum: 0
type: integer
droppedLabels:
items:
type: string
nullable: true
type: array
effectiveFrom:
format: date-time
type: string
ingestedSeries:
minimum: 0
type: integer
reductionPercent:
format: double
type: number
retainedSeries:
minimum: 0
type: integer
required:
- ingestedSeries
- currentRetainedSeries
- retainedSeries
- reductionPercent
- droppedLabels
- affectedAssets
- effectiveFrom
type: object
MetricreductionruletypesGettableReductionRuleStats:
properties:
estimatedMonthlySavingsUsd:
format: double
type: number
ingestedSeries:
minimum: 0
type: integer
retainedSeries:
minimum: 0
type: integer
required:
- ingestedSeries
- retainedSeries
- estimatedMonthlySavingsUsd
type: object
MetricreductionruletypesGettableReductionRules:
properties:
rules:
items:
$ref: '#/components/schemas/MetricreductionruletypesGettableReductionRule'
nullable: true
type: array
total:
type: integer
required:
- rules
- total
type: object
MetricreductionruletypesMatchType:
enum:
- drop
- keep
type: string
MetricreductionruletypesOrder:
enum:
- asc
- desc
type: string
MetricreductionruletypesPostableReductionRule:
properties:
labels:
items:
type: string
nullable: true
type: array
matchType:
$ref: '#/components/schemas/MetricreductionruletypesMatchType'
metricName:
type: string
required:
- metricName
- matchType
- labels
type: object
MetricreductionruletypesPostableReductionRulePreview:
properties:
labels:
items:
type: string
nullable: true
type: array
lookbackMs:
format: int64
type: integer
matchType:
$ref: '#/components/schemas/MetricreductionruletypesMatchType'
metricName:
type: string
required:
- metricName
- matchType
- labels
type: object
MetricreductionruletypesReductionRuleOrderBy:
enum:
- metric
- ingested_volume
- reduced_volume
- reduction
- last_updated
type: string
MetricreductionruletypesUpdatableReductionRule:
properties:
labels:
items:
type: string
nullable: true
type: array
matchType:
$ref: '#/components/schemas/MetricreductionruletypesMatchType'
required:
- matchType
- labels
type: object
MetricsexplorertypesInspectMetricsRequest:
properties:
end:
@@ -15824,6 +16039,566 @@ paths:
summary: Liveness check
tags:
- health
/api/v2/metric_reduction_rules:
get:
deprecated: false
description: Returns active metric volume-control (label reduction) rules.
operationId: ListMetricReductionRules
parameters:
- in: query
name: orderBy
schema:
$ref: '#/components/schemas/MetricreductionruletypesReductionRuleOrderBy'
- in: query
name: order
schema:
$ref: '#/components/schemas/MetricreductionruletypesOrder'
- in: query
name: search
schema:
type: string
- in: query
name: metricName
schema:
type: string
- in: query
name: offset
schema:
type: integer
- in: query
name: limit
schema:
type: integer
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/MetricreductionruletypesGettableReductionRules'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"451":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unavailable For Legal Reasons
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
"501":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Implemented
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: List metric reduction rules
tags:
- metrics
post:
deprecated: false
description: Creates a volume-control rule for a metric and returns it with
its id; fails if the metric already has a rule.
operationId: CreateMetricReductionRule
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/MetricreductionruletypesPostableReductionRule'
responses:
"201":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/MetricreductionruletypesGettableReductionRule'
status:
type: string
required:
- status
- data
type: object
description: Created
"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
"409":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Conflict
"451":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unavailable For Legal Reasons
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
"501":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Implemented
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Create a metric reduction rule
tags:
- metrics
/api/v2/metric_reduction_rules/{id}:
delete:
deprecated: false
description: Deletes a volume-control rule by its id.
operationId: DeleteMetricReductionRuleByID
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"204":
description: No Content
"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
"451":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unavailable For Legal Reasons
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
"501":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Implemented
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Delete a metric reduction rule by id
tags:
- metrics
get:
deprecated: false
description: Returns a single volume-control rule by its id.
operationId: GetMetricReductionRuleByID
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/MetricreductionruletypesGettableReductionRule'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"451":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unavailable For Legal Reasons
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
"501":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Implemented
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get a metric reduction rule by id
tags:
- metrics
put:
deprecated: false
description: Updates the match type and labels of a volume-control rule by its
id; the metric name is immutable.
operationId: UpdateMetricReductionRuleByID
parameters:
- in: path
name: id
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/MetricreductionruletypesUpdatableReductionRule'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/MetricreductionruletypesGettableReductionRule'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"451":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unavailable For Legal Reasons
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
"501":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Implemented
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Update a metric reduction rule by id
tags:
- metrics
/api/v2/metric_reduction_rules/preview:
post:
deprecated: false
description: Estimates the series reduction and related-asset impact of a candidate
volume-control rule without persisting it.
operationId: PreviewMetricReductionRule
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/MetricreductionruletypesPostableReductionRulePreview'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/MetricreductionruletypesGettableReductionRulePreview'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"451":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unavailable For Legal Reasons
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
"501":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Implemented
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Preview a metric reduction rule
tags:
- metrics
/api/v2/metric_reduction_rules/stats:
get:
deprecated: false
description: Returns total ingested vs retained series and the estimated monthly
savings across all volume-control rules.
operationId: GetMetricReductionRuleStats
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/MetricreductionruletypesGettableReductionRuleStats'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"451":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unavailable For Legal Reasons
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
"501":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Implemented
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Metric reduction stats
tags:
- metrics
/api/v2/metric_reduction_rules/timeseries:
get:
deprecated: false
description: Returns ingested vs retained series over time across all volume-control
rules (hourly buckets), in the query-range time-series response shape.
operationId: GetMetricReductionRuleTimeseries
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/Querybuildertypesv5QueryRangeResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"451":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unavailable For Legal Reasons
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
"501":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Implemented
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Metric reduction volume over time
tags:
- metrics
/api/v2/metrics:
get:
deprecated: false

View File

@@ -374,7 +374,7 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
}
for _, cb := range provider.onBeforeRoleDelete {
if err := cb(ctx, orgID, id); err != nil {
if err := cb(ctx, orgID, id, role.Name); err != nil {
return err
}
}

View File

@@ -0,0 +1,565 @@
package implmetricreductionrule
import (
"context"
"slices"
"time"
sqlbuilder "github.com/huandu/go-sqlbuilder"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/telemetrymetrics"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"github.com/SigNoz/signoz/pkg/types/metricreductionruletypes"
)
var (
reductionRulesTable = telemetrymetrics.DBName + "." + telemetrymetrics.ReductionRulesTableName
metadataTable = telemetrymetrics.DBName + "." + telemetrymetrics.AttributesMetadataTableName
bufferSeriesTable = telemetrymetrics.DBName + "." + telemetrymetrics.TimeseriesV4BufferTableName
)
const timeSeriesBucketMilli = int64(time.Hour / time.Millisecond)
type volumeRow struct {
MetricName string
Ingested uint64
Reduced uint64
}
type volumePoint struct {
TimestampMs int64
Ingested uint64
Reduced uint64
}
type clickhouse struct {
telemetryStore telemetrystore.TelemetryStore
threads int
}
func newClickhouse(telemetryStore telemetrystore.TelemetryStore, threads int) *clickhouse {
return &clickhouse{telemetryStore: telemetryStore, threads: threads}
}
func (c *clickhouse) withThreads(ctx context.Context) context.Context {
return ctxtypes.SetClickhouseMaxThreads(ctx, c.threads)
}
func floorToTimeSeriesBucket(ms int64) int64 {
return ms - (ms % timeSeriesBucketMilli)
}
func strictEffectiveFrom(sb *sqlbuilder.SelectBuilder, metricNames []string, effectiveFrom map[string]int64) string {
names := make([]any, 0, len(metricNames))
froms := make([]any, 0, len(metricNames))
for _, name := range metricNames {
names = append(names, name)
froms = append(froms, effectiveFrom[name])
}
return "unix_milli >= transform(metric_name, " + sb.Var(names) + ", " + sb.Var(froms) + ", 0)"
}
func (c *clickhouse) Sync(ctx context.Context, metricName string, labels []string, matchType string, effectiveFromMs int64, deleted bool, updatedAt time.Time) error {
ctx = c.withThreads(ctx)
ib := sqlbuilder.NewInsertBuilder()
ib.InsertInto(reductionRulesTable)
ib.Cols("metric_name", "labels", "match_type", "effective_from_unix_milli", "deleted", "updated_at")
ib.Values(metricName, labels, matchType, effectiveFromMs, deleted, updatedAt)
query, args := ib.BuildWithFlavor(sqlbuilder.ClickHouse)
if err := c.telemetryStore.ClickhouseDB().Exec(ctx, query, args...); err != nil {
return errors.WrapInternalf(err, errors.CodeInternal, "failed to sync reduction rule to clickhouse")
}
return nil
}
func (c *clickhouse) AttributeKeys(ctx context.Context, metricName string, startMs, endMs int64) ([]string, error) {
ctx = c.withThreads(ctx)
sb := sqlbuilder.NewSelectBuilder()
sb.Select("attr_name")
sb.Distinct()
sb.From(metadataTable)
sb.Where(
sb.E("metric_name", metricName),
"NOT startsWith(attr_name, '__')",
sb.GE("last_reported_unix_milli", startMs),
sb.LE("first_reported_unix_milli", endMs),
)
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
rows, err := c.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to fetch metric attribute keys")
}
defer rows.Close()
keys := make([]string, 0)
for rows.Next() {
var key string
if err := rows.Scan(&key); err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to scan attribute key")
}
keys = append(keys, key)
}
return keys, rows.Err()
}
func (c *clickhouse) EstimateCardinality(ctx context.Context, metricName string, keptLabels []string, startMs, endMs int64) (uint64, uint64, error) {
ctx = c.withThreads(ctx)
startMs = floorToTimeSeriesBucket(startMs)
sb := sqlbuilder.NewSelectBuilder()
reducedExpr := "1"
if len(keptLabels) > 0 {
reducedExpr = "uniq(("
for i, label := range keptLabels {
if i > 0 {
reducedExpr += ", "
}
reducedExpr += "JSONExtractString(labels, " + sb.Var(label) + ")"
}
reducedExpr += "))"
}
sb.Select("uniq(fingerprint)", reducedExpr)
sb.From(bufferSeriesTable)
conds := []string{
sb.E("metric_name", metricName),
sb.GE("unix_milli", startMs),
sb.LT("unix_milli", endMs),
sb.E("is_reduced", false),
}
sb.Where(conds...)
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
var current, reduced uint64
if err := c.telemetryStore.ClickhouseDB().QueryRow(ctx, query, args...).Scan(&current, &reduced); err != nil {
return 0, 0, errors.WrapInternalf(err, errors.CodeInternal, "failed to estimate reduction impact")
}
if len(keptLabels) == 0 && current == 0 {
reduced = 0
}
if reduced > current {
reduced = current
}
return current, reduced, nil
}
// VolumeByMetric returns ingested vs reduced series counts per metric.
func (c *clickhouse) VolumeByMetric(ctx context.Context, metricNames []string, effectiveFrom map[string]int64, startMs, endMs int64) (map[string]volumeRow, error) {
if len(metricNames) == 0 {
return map[string]volumeRow{}, nil
}
ctx = c.withThreads(ctx)
ingested, err := c.ingestedSeriesCount(ctx, metricNames, effectiveFrom, startMs, endMs)
if err != nil {
return nil, err
}
reduced, err := c.reducedSeriesCount(ctx, metricNames, effectiveFrom, startMs, endMs)
if err != nil {
return nil, err
}
out := make(map[string]volumeRow, len(metricNames))
for metricName, count := range ingested {
out[metricName] = volumeRow{MetricName: metricName, Ingested: count, Reduced: out[metricName].Reduced}
}
for metricName, count := range reduced {
row := out[metricName]
row.MetricName = metricName
row.Reduced = count
out[metricName] = row
}
return out, nil
}
// ingestedSeriesCount counts distinct raw fingerprints per metric from the samples buffer over the
// window.
func (c *clickhouse) ingestedSeriesCount(ctx context.Context, metricNames []string, effectiveFrom map[string]int64, startMs, endMs int64) (map[string]uint64, error) {
names := make([]any, len(metricNames))
for i, name := range metricNames {
names[i] = name
}
sb := sqlbuilder.NewSelectBuilder()
sb.Select("metric_name", "uniq(fingerprint)")
sb.From(telemetrymetrics.DBName + "." + telemetrymetrics.SamplesV4BufferTableName)
conds := []string{
sb.In("metric_name", names...),
sb.GE("unix_milli", startMs),
sb.LT("unix_milli", endMs),
}
if len(effectiveFrom) > 0 {
conds = append(conds, strictEffectiveFrom(sb, metricNames, effectiveFrom))
}
sb.Where(conds...)
sb.GroupBy("metric_name")
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
rows, err := c.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to count ingested series")
}
defer rows.Close()
out := make(map[string]uint64, len(metricNames))
for rows.Next() {
var (
metricName string
count uint64
)
if err := rows.Scan(&metricName, &count); err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to scan series count")
}
out[metricName] = count
}
return out, rows.Err()
}
// reducedSeriesCount counts distinct reduced_fingerprints per metric, summed across the two 60s
// reduced sample tables.
func (c *clickhouse) reducedSeriesCount(ctx context.Context, metricNames []string, effectiveFrom map[string]int64, startMs, endMs int64) (map[string]uint64, error) {
out := make(map[string]uint64, len(metricNames))
for _, table := range []string{telemetrymetrics.SamplesV4ReducedLastTableName, telemetrymetrics.SamplesV4ReducedSumTableName} {
counts, err := c.reducedSeriesCountForTable(ctx, telemetrymetrics.DBName+"."+table, metricNames, effectiveFrom, startMs, endMs)
if err != nil {
return nil, err
}
for metricName, count := range counts {
out[metricName] += count
}
}
return out, nil
}
func (c *clickhouse) reducedSeriesCountForTable(ctx context.Context, table string, metricNames []string, effectiveFrom map[string]int64, startMs, endMs int64) (map[string]uint64, error) {
names := make([]any, len(metricNames))
for i, name := range metricNames {
names[i] = name
}
sb := sqlbuilder.NewSelectBuilder()
sb.Select("metric_name", "uniq(reduced_fingerprint)")
sb.From(table)
conds := []string{
sb.In("metric_name", names...),
sb.GE("unix_milli", startMs),
sb.LT("unix_milli", endMs),
}
if len(effectiveFrom) > 0 {
conds = append(conds, strictEffectiveFrom(sb, metricNames, effectiveFrom))
}
sb.Where(conds...)
sb.GroupBy("metric_name")
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
rows, err := c.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to count reduced series")
}
defer rows.Close()
out := make(map[string]uint64, len(metricNames))
for rows.Next() {
var (
metricName string
count uint64
)
if err := rows.Scan(&metricName, &count); err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to scan series count")
}
out[metricName] = count
}
return out, rows.Err()
}
// RankByVolume ranks metrics by ingested/reduced series volume. Like VolumeByMetric, the counts read
// the samples tables with a strict effective_from gate; the reduced count sums distinct
// reduced_fingerprints across the two 60s reduced sample tables.
func (c *clickhouse) RankByVolume(ctx context.Context, metricNames []string, effectiveFrom map[string]int64, orderBy metricreductionruletypes.ReductionRuleOrderBy, order metricreductionruletypes.Order, startMs, endMs int64, offset, limit int) ([]volumeRow, error) {
if len(metricNames) == 0 {
return []volumeRow{}, nil
}
ctx = c.withThreads(ctx)
orderExpr := "ingested"
switch orderBy {
case metricreductionruletypes.OrderByReducedVolume:
orderExpr = "reduced"
case metricreductionruletypes.OrderByReduction:
orderExpr = "if(ingested = 0, 0, (toFloat64(ingested) - toFloat64(reduced)) / toFloat64(ingested))"
}
direction := "ASC"
if order == metricreductionruletypes.OrderDesc {
direction = "DESC"
}
ingestedTable := telemetrymetrics.DBName + "." + telemetrymetrics.SamplesV4BufferTableName
reducedLast := telemetrymetrics.DBName + "." + telemetrymetrics.SamplesV4ReducedLastTableName
reducedSum := telemetrymetrics.DBName + "." + telemetrymetrics.SamplesV4ReducedSumTableName
sb := sqlbuilder.NewSelectBuilder()
sb.Select("base.metric_name AS metric_name", "ifNull(i.cnt, 0) AS ingested", "ifNull(d.cnt, 0) AS reduced")
sb.From("(SELECT arrayJoin(" + sb.Var(metricNames) + ") AS metric_name) AS base")
sb.JoinWithOption(
sqlbuilder.LeftJoin,
"(SELECT metric_name, uniq(fingerprint) AS cnt FROM "+ingestedTable+" WHERE has("+sb.Var(metricNames)+", metric_name) AND unix_milli >= "+sb.Var(startMs)+" AND unix_milli < "+sb.Var(endMs)+" AND "+strictEffectiveFrom(sb, metricNames, effectiveFrom)+" GROUP BY metric_name) AS i",
"base.metric_name = i.metric_name",
)
// Reduced series are spread across two type-specific tables; union the per-table distinct
// reduced_fingerprints and sum per metric (a metric only lands in the table matching its type).
sb.JoinWithOption(
sqlbuilder.LeftJoin,
"(SELECT metric_name, sum(cnt) AS cnt FROM ("+
"SELECT metric_name, uniq(reduced_fingerprint) AS cnt FROM "+reducedLast+" WHERE has("+sb.Var(metricNames)+", metric_name) AND unix_milli >= "+sb.Var(startMs)+" AND unix_milli < "+sb.Var(endMs)+" AND "+strictEffectiveFrom(sb, metricNames, effectiveFrom)+" GROUP BY metric_name"+
" UNION ALL "+
"SELECT metric_name, uniq(reduced_fingerprint) AS cnt FROM "+reducedSum+" WHERE has("+sb.Var(metricNames)+", metric_name) AND unix_milli >= "+sb.Var(startMs)+" AND unix_milli < "+sb.Var(endMs)+" AND "+strictEffectiveFrom(sb, metricNames, effectiveFrom)+" GROUP BY metric_name"+
") GROUP BY metric_name) AS d",
"base.metric_name = d.metric_name",
)
sb.OrderBy(orderExpr + " " + direction)
if limit > 0 {
sb.Limit(limit).Offset(offset)
}
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
rows, err := c.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to rank reduction rules by volume")
}
defer rows.Close()
out := make([]volumeRow, 0, len(metricNames))
for rows.Next() {
var row volumeRow
if err := rows.Scan(&row.MetricName, &row.Ingested, &row.Reduced); err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to scan volume row")
}
out = append(out, row)
}
return out, rows.Err()
}
func (c *clickhouse) SampleVolume(ctx context.Context, metricNames []string, effectiveFrom map[string]int64, startMs, endMs int64) (uint64, uint64, error) {
if len(metricNames) == 0 {
return 0, 0, nil
}
ctx = c.withThreads(ctx)
ingested, err := c.countRawSamples(ctx, telemetrymetrics.DBName+"."+telemetrymetrics.SamplesV4BufferTableName, metricNames, effectiveFrom, startMs, endMs)
if err != nil {
return 0, 0, err
}
last, err := c.countReducedSamples(ctx, telemetrymetrics.DBName+"."+telemetrymetrics.SamplesV4ReducedLastTableName, metricNames, effectiveFrom, startMs, endMs)
if err != nil {
return 0, 0, err
}
sum, err := c.countReducedSamples(ctx, telemetrymetrics.DBName+"."+telemetrymetrics.SamplesV4ReducedSumTableName, metricNames, effectiveFrom, startMs, endMs)
if err != nil {
return 0, 0, err
}
return ingested, min(last+sum, ingested), nil
}
func (c *clickhouse) countRawSamples(ctx context.Context, table string, metricNames []string, effectiveFrom map[string]int64, startMs, endMs int64) (uint64, error) {
names := make([]any, len(metricNames))
for i, name := range metricNames {
names[i] = name
}
sb := sqlbuilder.NewSelectBuilder()
sb.Select("count()")
sb.From(table)
conds := []string{sb.In("metric_name", names...), sb.GE("unix_milli", startMs), sb.LT("unix_milli", endMs)}
if len(effectiveFrom) > 0 {
conds = append(conds, strictEffectiveFrom(sb, metricNames, effectiveFrom))
}
sb.Where(conds...)
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
var count uint64
if err := c.telemetryStore.ClickhouseDB().QueryRow(ctx, query, args...).Scan(&count); err != nil {
return 0, errors.WrapInternalf(err, errors.CodeInternal, "failed to count ingested samples")
}
return count, nil
}
func (c *clickhouse) countReducedSamples(ctx context.Context, table string, metricNames []string, effectiveFrom map[string]int64, startMs, endMs int64) (uint64, error) {
names := make([]any, len(metricNames))
for i, name := range metricNames {
names[i] = name
}
sb := sqlbuilder.NewSelectBuilder()
// Reduced tables key the series on reduced_fingerprint (not fingerprint); dedupe ReplacingMergeTree recomputes.
sb.Select("uniq(reduced_fingerprint, unix_milli)")
sb.From(table)
conds := []string{sb.In("metric_name", names...), sb.GE("unix_milli", startMs), sb.LT("unix_milli", endMs)}
if len(effectiveFrom) > 0 {
conds = append(conds, strictEffectiveFrom(sb, metricNames, effectiveFrom))
}
sb.Where(conds...)
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
var count uint64
if err := c.telemetryStore.ClickhouseDB().QueryRow(ctx, query, args...).Scan(&count); err != nil {
return 0, errors.WrapInternalf(err, errors.CodeInternal, "failed to count reduced samples")
}
return count, nil
}
// SeriesTimeseries returns ingested vs reduced series per 60s bucket from the samples tables, gated
// to each metric's strict effective_from (see strictEffectiveFrom).
func (c *clickhouse) SeriesTimeseries(ctx context.Context, allMetrics, reducedMetrics []string, effectiveFrom map[string]int64, startMs, endMs int64) ([]volumePoint, error) {
if len(allMetrics) == 0 {
return []volumePoint{}, nil
}
ctx = c.withThreads(ctx)
ingested, err := c.ingestedSeriesByBucket(ctx, allMetrics, effectiveFrom, startMs, endMs)
if err != nil {
return nil, err
}
retained := make(map[int64]uint64)
if len(reducedMetrics) > 0 {
reduced, err := c.reducedSeriesByBucket(ctx, reducedMetrics, effectiveFrom, startMs, endMs)
if err != nil {
return nil, err
}
for ts, count := range reduced {
retained[ts] += count
}
}
reducedSet := make(map[string]struct{}, len(reducedMetrics))
for _, name := range reducedMetrics {
reducedSet[name] = struct{}{}
}
nonReduced := make([]string, 0, len(allMetrics))
for _, name := range allMetrics {
if _, ok := reducedSet[name]; !ok {
nonReduced = append(nonReduced, name)
}
}
if len(nonReduced) > 0 {
nonReducedIngested, err := c.ingestedSeriesByBucket(ctx, nonReduced, effectiveFrom, startMs, endMs)
if err != nil {
return nil, err
}
for ts, count := range nonReducedIngested {
retained[ts] += count
}
}
return mergeVolumePoints(ingested, retained), nil
}
func mergeVolumePoints(ingested, reduced map[int64]uint64) []volumePoint {
buckets := make(map[int64]struct{}, len(ingested))
for ts := range ingested {
buckets[ts] = struct{}{}
}
for ts := range reduced {
buckets[ts] = struct{}{}
}
timestamps := make([]int64, 0, len(buckets))
for ts := range buckets {
timestamps = append(timestamps, ts)
}
slices.Sort(timestamps)
points := make([]volumePoint, 0, len(timestamps))
for _, ts := range timestamps {
points = append(points, volumePoint{
TimestampMs: ts,
Ingested: ingested[ts],
Reduced: reduced[ts],
})
}
return points
}
// ingestedSeriesByBucket counts distinct raw fingerprints per hourly bucket from the samples buffer.
func (c *clickhouse) ingestedSeriesByBucket(ctx context.Context, metricNames []string, effectiveFrom map[string]int64, startMs, endMs int64) (map[int64]uint64, error) {
names := make([]any, len(metricNames))
for i, name := range metricNames {
names[i] = name
}
sb := sqlbuilder.NewSelectBuilder()
bucketExpr := "toInt64(toUnixTimestamp(toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalHour(1)))) * 1000 AS bucket"
sb.Select(bucketExpr, "uniq(fingerprint)")
sb.From(telemetrymetrics.DBName + "." + telemetrymetrics.SamplesV4BufferTableName)
conds := []string{sb.In("metric_name", names...), sb.GE("unix_milli", startMs), sb.LT("unix_milli", endMs)}
if len(effectiveFrom) > 0 {
conds = append(conds, strictEffectiveFrom(sb, metricNames, effectiveFrom))
}
sb.Where(conds...)
sb.GroupBy("bucket")
return c.scanBuckets(ctx, sb)
}
// reducedSeriesByBucket counts distinct reduced_fingerprints per hourly bucket, summed across the two
// reduced sample tables (a metric only lands in the table matching its type, so per-bucket sums are
// exact).
func (c *clickhouse) reducedSeriesByBucket(ctx context.Context, metricNames []string, effectiveFrom map[string]int64, startMs, endMs int64) (map[int64]uint64, error) {
out := make(map[int64]uint64)
for _, table := range []string{telemetrymetrics.SamplesV4ReducedLastTableName, telemetrymetrics.SamplesV4ReducedSumTableName} {
names := make([]any, len(metricNames))
for i, name := range metricNames {
names[i] = name
}
sb := sqlbuilder.NewSelectBuilder()
bucketExpr := "toInt64(toUnixTimestamp(toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalHour(1)))) * 1000 AS bucket"
sb.Select(bucketExpr, "uniq(reduced_fingerprint)")
sb.From(telemetrymetrics.DBName + "." + table)
conds := []string{sb.In("metric_name", names...), sb.GE("unix_milli", startMs), sb.LT("unix_milli", endMs)}
if len(effectiveFrom) > 0 {
conds = append(conds, strictEffectiveFrom(sb, metricNames, effectiveFrom))
}
sb.Where(conds...)
sb.GroupBy("bucket")
counts, err := c.scanBuckets(ctx, sb)
if err != nil {
return nil, err
}
for ts, count := range counts {
out[ts] += count
}
}
return out, nil
}
func (c *clickhouse) scanBuckets(ctx context.Context, sb *sqlbuilder.SelectBuilder) (map[int64]uint64, error) {
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
rows, err := c.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to bucket series by time")
}
defer rows.Close()
out := make(map[int64]uint64)
for rows.Next() {
var (
ts int64
count uint64
)
if err := rows.Scan(&ts, &count); err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to scan series bucket")
}
out[ts] = count
}
return out, rows.Err()
}

View File

@@ -0,0 +1,571 @@
package implmetricreductionrule
import (
"context"
"log/slog"
"sort"
"strings"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/metricreductionrule"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/ruler/rulestore/sqlrulestore"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/metricreductionruletypes"
"github.com/SigNoz/signoz/pkg/types/metrictypes"
"github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
const (
// effectiveFromMargin delays effective_from so the collector picks up the synced rule before it
// goes live; it must be >= the collector's rule-refresh interval (see signoz-otel-collector#839).
effectiveFromMargin = 5 * time.Minute
defaultPreviewLookback = 24 * time.Hour
pricePerMillionSamplesUSD = 0.1
monthDuration = 30 * 24 * time.Hour
)
type module struct {
store metricreductionruletypes.Store
ch *clickhouse
dashboard dashboard.Module
ruleStore ruletypes.RuleStore
licensing licensing.Licensing
flagger flagger.Flagger
metadataStore telemetrytypes.MetadataStore
logger *slog.Logger
}
func NewModule(sqlStore sqlstore.SQLStore, telemetryStore telemetrystore.TelemetryStore, dashboardModule dashboard.Module, queryParser queryparser.QueryParser, licensing licensing.Licensing, flagger flagger.Flagger, metadataStore telemetrytypes.MetadataStore, providerSettings factory.ProviderSettings, threads int) metricreductionrule.Module {
scoped := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/ee/modules/metricreductionrule/implmetricreductionrule")
return &module{
store: NewStore(sqlStore),
ch: newClickhouse(telemetryStore, threads),
dashboard: dashboardModule,
ruleStore: sqlrulestore.NewRuleStore(sqlStore, queryParser, providerSettings),
licensing: licensing,
flagger: flagger,
metadataStore: metadataStore,
logger: scoped.Logger(),
}
}
func (m *module) checkAccess(ctx context.Context, orgID valuer.UUID) error {
if !m.flagger.BooleanOrEmpty(ctx, flagger.FeatureEnableMetricsReduction, featuretypes.NewFlaggerEvaluationContext(orgID)) {
return errors.Newf(errors.TypeUnsupported, metricreductionruletypes.ErrCodeMetricReductionRuleUnsupported, "metric volume control is not enabled")
}
if _, err := m.licensing.GetActive(ctx, orgID); err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "metric volume control requires a valid license").WithAdditional(err.Error())
}
return nil
}
func (m *module) List(ctx context.Context, orgID valuer.UUID, params *metricreductionruletypes.ListReductionRulesParams) (*metricreductionruletypes.GettableReductionRules, error) {
if err := m.checkAccess(ctx, orgID); err != nil {
return nil, err
}
if err := params.Validate(); err != nil {
return nil, err
}
now := time.Now()
startMs := now.Add(-defaultPreviewLookback).UnixMilli()
endMs := now.UnixMilli()
switch params.OrderBy {
case metricreductionruletypes.OrderByMetricName, metricreductionruletypes.OrderByLastUpdated:
return m.listSortedByColumn(ctx, orgID, params, startMs, endMs)
default:
return m.listSortedByVolume(ctx, orgID, params, startMs, endMs)
}
}
func (m *module) listSortedByColumn(ctx context.Context, orgID valuer.UUID, params *metricreductionruletypes.ListReductionRulesParams, startMs, endMs int64) (*metricreductionruletypes.GettableReductionRules, error) {
domainRules, total, err := m.store.List(ctx, orgID, params)
if err != nil {
return nil, err
}
metricNames := make([]string, len(domainRules))
effectiveFrom := make(map[string]int64, len(domainRules))
for i, rule := range domainRules {
metricNames[i] = rule.MetricName
effectiveFrom[rule.MetricName] = rule.EffectiveFrom.UnixMilli()
}
volumes, err := m.ch.VolumeByMetric(ctx, metricNames, effectiveFrom, startMs, endMs)
if err != nil {
return nil, err
}
rules := make([]metricreductionruletypes.GettableReductionRule, 0, len(domainRules))
for _, rule := range domainRules {
rules = append(rules, withVolume(toGettableReductionRule(rule), volumes[rule.MetricName]))
}
return &metricreductionruletypes.GettableReductionRules{Rules: rules, Total: total}, nil
}
func (m *module) listSortedByVolume(ctx context.Context, orgID valuer.UUID, params *metricreductionruletypes.ListReductionRulesParams, startMs, endMs int64) (*metricreductionruletypes.GettableReductionRules, error) {
allRules, total, err := m.store.List(ctx, orgID, &metricreductionruletypes.ListReductionRulesParams{Search: params.Search, MetricName: params.MetricName})
if err != nil {
return nil, err
}
if total == 0 {
return &metricreductionruletypes.GettableReductionRules{Rules: []metricreductionruletypes.GettableReductionRule{}, Total: 0}, nil
}
metricNames := make([]string, len(allRules))
effectiveFrom := make(map[string]int64, len(allRules))
ruleByMetric := make(map[string]*metricreductionruletypes.ReductionRule, len(allRules))
for i, rule := range allRules {
metricNames[i] = rule.MetricName
effectiveFrom[rule.MetricName] = rule.EffectiveFrom.UnixMilli()
ruleByMetric[rule.MetricName] = rule
}
ranked, err := m.ch.RankByVolume(ctx, metricNames, effectiveFrom, params.OrderBy, params.Order, startMs, endMs, params.Offset, params.Limit)
if err != nil {
return nil, err
}
rules := make([]metricreductionruletypes.GettableReductionRule, 0, len(ranked))
for _, row := range ranked {
rule, ok := ruleByMetric[row.MetricName]
if !ok {
continue
}
rules = append(rules, withVolume(toGettableReductionRule(rule), row))
}
return &metricreductionruletypes.GettableReductionRules{Rules: rules, Total: total}, nil
}
func (m *module) Create(ctx context.Context, orgID valuer.UUID, userEmail string, req *metricreductionruletypes.PostableReductionRule) (*metricreductionruletypes.GettableReductionRule, error) {
if err := m.checkAccess(ctx, orgID); err != nil {
return nil, err
}
if err := req.Validate(); err != nil {
return nil, err
}
if err := m.validateMetricForReduction(ctx, orgID, req.MetricName); err != nil {
return nil, err
}
now := time.Now()
rule := metricreductionruletypes.NewReductionRule(orgID, req.MetricName, req.MatchType, req.Labels, now.Add(effectiveFromMargin), userEmail)
if err := m.store.RunInTx(ctx, func(ctx context.Context) error {
if err := m.store.Create(ctx, rule); err != nil {
return err
}
return m.ch.Sync(ctx, rule.MetricName, rule.Labels, rule.MatchType.StringValue(), rule.EffectiveFrom.UnixMilli(), false, rule.UpdatedAt)
}); err != nil {
return nil, err
}
gettable := toGettableReductionRule(rule)
return &gettable, nil
}
func (m *module) GetByID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*metricreductionruletypes.GettableReductionRule, error) {
if err := m.checkAccess(ctx, orgID); err != nil {
return nil, err
}
rule, err := m.store.GetByID(ctx, orgID, id)
if err != nil {
return nil, err
}
gettable := toGettableReductionRule(rule)
return &gettable, nil
}
func (m *module) UpdateByID(ctx context.Context, orgID valuer.UUID, userEmail string, id valuer.UUID, req *metricreductionruletypes.UpdatableReductionRule) (*metricreductionruletypes.GettableReductionRule, error) {
if err := m.checkAccess(ctx, orgID); err != nil {
return nil, err
}
existing, err := m.store.GetByID(ctx, orgID, id)
if err != nil {
return nil, err
}
if err := req.Validate(); err != nil {
return nil, err
}
now := time.Now()
existing.MatchType = req.MatchType
existing.Labels = metricreductionruletypes.LabelList(req.Labels)
existing.EffectiveFrom = now.Add(effectiveFromMargin)
existing.UpdatedAt = now
existing.UpdatedBy = userEmail
if err := m.store.RunInTx(ctx, func(ctx context.Context) error {
if err := m.store.Upsert(ctx, existing); err != nil {
return err
}
return m.ch.Sync(ctx, existing.MetricName, existing.Labels, existing.MatchType.StringValue(), existing.EffectiveFrom.UnixMilli(), false, existing.UpdatedAt)
}); err != nil {
return nil, err
}
gettable := toGettableReductionRule(existing)
return &gettable, nil
}
func (m *module) DeleteByID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
if err := m.checkAccess(ctx, orgID); err != nil {
return err
}
rule, err := m.store.GetByID(ctx, orgID, id)
if err != nil {
return err
}
now := time.Now()
effectiveFromMs := now.Add(effectiveFromMargin).UnixMilli()
return m.store.RunInTx(ctx, func(ctx context.Context) error {
if err := m.store.DeleteByID(ctx, orgID, id); err != nil {
return err
}
return m.ch.Sync(ctx, rule.MetricName, []string{}, metricreductionruletypes.MatchTypeDrop.StringValue(), effectiveFromMs, true, now)
})
}
func (m *module) Preview(ctx context.Context, orgID valuer.UUID, req *metricreductionruletypes.PostableReductionRulePreview) (*metricreductionruletypes.GettableReductionRulePreview, error) {
if err := m.checkAccess(ctx, orgID); err != nil {
return nil, err
}
if err := req.Validate(); err != nil {
return nil, err
}
if err := m.validateMetricForReduction(ctx, orgID, req.MetricName); err != nil {
return nil, err
}
lookback := time.Duration(req.LookbackMs) * time.Millisecond
if lookback <= 0 {
lookback = defaultPreviewLookback
}
now := time.Now()
startMs := now.Add(-lookback).UnixMilli()
endMs := now.UnixMilli()
current, reduced, reductionPercent, dropped, err := m.estimateVolume(ctx, req.MetricName, req.MatchType, req.Labels, startMs, endMs)
if err != nil {
return nil, err
}
// Baseline is what the metric keeps today (its current rule, or raw if none) so the preview reads
// as current -> proposed.
currentReduced := current
if existing, gerr := m.store.Get(ctx, orgID, req.MetricName); gerr == nil {
if _, existingReduced, _, _, eerr := m.estimateVolume(ctx, req.MetricName, existing.MatchType, existing.Labels, startMs, endMs); eerr == nil {
currentReduced = existingReduced
}
}
return &metricreductionruletypes.GettableReductionRulePreview{
IngestedSeries: current,
CurrentRetainedSeries: currentReduced,
RetainedSeries: reduced,
ReductionPercent: reductionPercent,
DroppedLabels: dropped,
AffectedAssets: m.relatedAssetImpact(ctx, orgID, req.MetricName, dropped),
EffectiveFrom: now.Add(effectiveFromMargin),
}, nil
}
func (m *module) Stats(ctx context.Context, orgID valuer.UUID) (*metricreductionruletypes.GettableReductionRuleStats, error) {
if err := m.checkAccess(ctx, orgID); err != nil {
return nil, err
}
now := time.Now()
startMs := now.Add(-defaultPreviewLookback).UnixMilli()
endMs := now.UnixMilli()
allRules, total, err := m.store.List(ctx, orgID, &metricreductionruletypes.ListReductionRulesParams{})
if err != nil {
return nil, err
}
if total == 0 {
return &metricreductionruletypes.GettableReductionRuleStats{}, nil
}
metricNames := make([]string, len(allRules))
effectiveFrom := make(map[string]int64, len(allRules))
for i, rule := range allRules {
metricNames[i] = rule.MetricName
effectiveFrom[rule.MetricName] = rule.EffectiveFrom.UnixMilli()
}
volumes, err := m.ch.VolumeByMetric(ctx, metricNames, effectiveFrom, startMs, endMs)
if err != nil {
return nil, err
}
var ingestedSeries, retainedSeries uint64
reducedMetricNames := make([]string, 0, len(volumes))
reducedEffectiveFrom := make(map[string]int64, len(volumes))
for name, volume := range volumes {
ingestedSeries += volume.Ingested
retained := effectiveRetained(volume.Ingested, volume.Reduced)
retainedSeries += retained
if retained < volume.Ingested {
reducedMetricNames = append(reducedMetricNames, name)
reducedEffectiveFrom[name] = effectiveFrom[name]
}
}
ingestedSamples, reducedSamples, err := m.ch.SampleVolume(ctx, reducedMetricNames, reducedEffectiveFrom, startMs, endMs)
if err != nil {
return nil, err
}
return &metricreductionruletypes.GettableReductionRuleStats{
IngestedSeries: ingestedSeries,
RetainedSeries: retainedSeries,
EstimatedMonthlySavingsUsd: monthlySavingsUSD(ingestedSamples, reducedSamples, startMs, endMs),
}, nil
}
// monthlySavingsUSD extrapolates the windowed sample reduction to a monthly figure at the per-sample
// list price. Ingested is gated to effective_from upstream, so pre-activation hours don't inflate it.
func monthlySavingsUSD(ingestedSamples, reducedSamples uint64, startMs, endMs int64) float64 {
if reducedSamples >= ingestedSamples || endMs <= startMs {
return 0
}
savedSamples := float64(ingestedSamples - reducedSamples)
monthlySamples := savedSamples * float64(monthDuration.Milliseconds()) / float64(endMs-startMs)
return monthlySamples / 1_000_000 * pricePerMillionSamplesUSD
}
func (m *module) Timeseries(ctx context.Context, orgID valuer.UUID) (*querybuildertypesv5.QueryRangeResponse, error) {
if err := m.checkAccess(ctx, orgID); err != nil {
return nil, err
}
now := time.Now()
startMs := now.Add(-defaultPreviewLookback).UnixMilli()
endMs := now.UnixMilli()
allRules, _, err := m.store.List(ctx, orgID, &metricreductionruletypes.ListReductionRulesParams{})
if err != nil {
return nil, err
}
metricNames := make([]string, len(allRules))
effectiveFrom := make(map[string]int64, len(allRules))
for i, rule := range allRules {
metricNames[i] = rule.MetricName
effectiveFrom[rule.MetricName] = rule.EffectiveFrom.UnixMilli()
}
volumes, err := m.ch.VolumeByMetric(ctx, metricNames, effectiveFrom, startMs, endMs)
if err != nil {
return nil, err
}
reducedNames := make([]string, 0, len(volumes))
for name, volume := range volumes {
if effectiveRetained(volume.Ingested, volume.Reduced) < volume.Ingested {
reducedNames = append(reducedNames, name)
}
}
points, err := m.ch.SeriesTimeseries(ctx, metricNames, reducedNames, effectiveFrom, startMs, endMs)
if err != nil {
return nil, err
}
return buildVolumeTimeseries(points), nil
}
func buildVolumeTimeseries(points []volumePoint) *querybuildertypesv5.QueryRangeResponse {
ingested := make([]*querybuildertypesv5.TimeSeriesValue, 0, len(points))
reduced := make([]*querybuildertypesv5.TimeSeriesValue, 0, len(points))
for _, point := range points {
ingested = append(ingested, &querybuildertypesv5.TimeSeriesValue{Timestamp: point.TimestampMs, Value: float64(point.Ingested)})
reduced = append(reduced, &querybuildertypesv5.TimeSeriesValue{Timestamp: point.TimestampMs, Value: float64(point.Reduced)})
}
return &querybuildertypesv5.QueryRangeResponse{
Type: querybuildertypesv5.RequestTypeTimeSeries,
Data: querybuildertypesv5.QueryData{
Results: []any{
&querybuildertypesv5.TimeSeriesData{
QueryName: "reduction_volume",
Aggregations: []*querybuildertypesv5.AggregationBucket{
{
Series: []*querybuildertypesv5.TimeSeries{
{Labels: []*querybuildertypesv5.Label{{Key: telemetrytypes.TelemetryFieldKey{Name: "series"}, Value: "ingested"}}, Values: ingested},
{Labels: []*querybuildertypesv5.Label{{Key: telemetrytypes.TelemetryFieldKey{Name: "series"}, Value: "retained"}}, Values: reduced},
},
},
},
},
},
},
}
}
func (m *module) validateMetricForReduction(ctx context.Context, orgID valuer.UUID, metricName string) error {
lastSeen, err := m.metadataStore.FetchLastSeenInfoMulti(ctx, metricName)
if err != nil {
return err
}
if lastSeen[metricName] == 0 {
return errors.NewNotFoundf(errors.CodeNotFound, "metric not found: %q", metricName)
}
now := time.Now()
startTs := uint64(now.Add(-defaultPreviewLookback).UnixMilli())
endTs := uint64(now.UnixMilli())
_, types, _, err := m.metadataStore.FetchTemporalityAndTypeMulti(ctx, orgID, startTs, endTs, metricName)
if err != nil {
return err
}
if types[metricName] == metrictypes.ExpHistogramType {
return errors.Newf(errors.TypeInvalidInput, metricreductionruletypes.ErrCodeMetricReductionRuleUnsupportedMetricType,
"exponential histogram metrics cannot be reduced in v1")
}
return nil
}
func (m *module) relatedAssetImpact(ctx context.Context, orgID valuer.UUID, metricName string, dropped []string) []metricreductionruletypes.AffectedAsset {
affected := make([]metricreductionruletypes.AffectedAsset, 0)
droppedSet := make(map[string]struct{}, len(dropped))
for _, label := range dropped {
droppedSet[label] = struct{}{}
}
if dashboards, err := m.dashboard.GetByMetricNames(ctx, orgID, []string{metricName}); err != nil {
m.logger.WarnContext(ctx, "failed to fetch related dashboards for reduction preview", slog.String("metric_name", metricName), errors.Attr(err))
} else {
for _, item := range dashboards[metricName] {
usedLabels := append(splitCSV(item["group_by"]), splitCSV(item["filter_by"])...)
affected = append(affected, metricreductionruletypes.AffectedAsset{
Type: metricreductionruletypes.AssetTypeDashboard,
ID: item["dashboard_id"],
Name: item["dashboard_name"],
Widget: &metricreductionruletypes.AffectedWidget{ID: item["widget_id"], Name: item["widget_name"]},
ImpactedLabels: intersectLabels(usedLabels, droppedSet),
})
}
}
if alerts, err := m.ruleStore.GetStoredRulesByMetricName(ctx, orgID.String(), metricName); err != nil {
m.logger.WarnContext(ctx, "failed to fetch related alerts for reduction preview", slog.String("metric_name", metricName), errors.Attr(err))
} else {
for _, a := range alerts {
affected = append(affected, metricreductionruletypes.AffectedAsset{
Type: metricreductionruletypes.AssetTypeAlert,
ID: a.AlertID,
Name: a.AlertName,
})
}
}
return affected
}
func toGettableReductionRule(rule *metricreductionruletypes.ReductionRule) metricreductionruletypes.GettableReductionRule {
return metricreductionruletypes.GettableReductionRule{
Identifiable: rule.Identifiable,
TimeAuditable: rule.TimeAuditable,
UserAuditable: rule.UserAuditable,
MetricName: rule.MetricName,
MatchType: rule.MatchType,
Labels: rule.Labels,
EffectiveFrom: rule.EffectiveFrom,
Active: !rule.EffectiveFrom.After(time.Now()),
}
}
func effectiveRetained(ingested, reduced uint64) uint64 {
if reduced == 0 || reduced > ingested {
return ingested
}
return reduced
}
func withVolume(rule metricreductionruletypes.GettableReductionRule, volume volumeRow) metricreductionruletypes.GettableReductionRule {
rule.IngestedSeries = volume.Ingested
rule.RetainedSeries = effectiveRetained(volume.Ingested, volume.Reduced)
if volume.Ingested > 0 {
rule.ReductionPercent = (1 - float64(rule.RetainedSeries)/float64(volume.Ingested)) * 100
}
return rule
}
func intersectLabels(keys []string, droppedSet map[string]struct{}) []string {
seen := make(map[string]struct{})
var out []string
for _, key := range keys {
if _, ok := droppedSet[key]; !ok {
continue
}
if _, dup := seen[key]; dup {
continue
}
seen[key] = struct{}{}
out = append(out, key)
}
return out
}
func splitCSV(s string) []string {
if s == "" {
return nil
}
return strings.Split(s, ",")
}
func resolveDroppedKept(matchType metricreductionruletypes.MatchType, ruleLabels, keys []string) (dropped, kept []string) {
ruleSet := make(map[string]struct{}, len(ruleLabels))
for _, l := range ruleLabels {
ruleSet[l] = struct{}{}
}
for _, k := range keys {
if metricreductionruletypes.IsProtectedLabel(k) {
kept = append(kept, k)
continue
}
_, listed := ruleSet[k]
drop := listed
if matchType == metricreductionruletypes.MatchTypeKeep {
drop = !listed
}
if drop {
dropped = append(dropped, k)
} else {
kept = append(kept, k)
}
}
sort.Strings(dropped)
sort.Strings(kept)
return dropped, kept
}
func (m *module) estimateVolume(ctx context.Context, metricName string, matchType metricreductionruletypes.MatchType, labels []string, startMs, endMs int64) (current uint64, reduced uint64, reductionPercent float64, dropped []string, err error) {
keys, err := m.ch.AttributeKeys(ctx, metricName, startMs, endMs)
if err != nil {
return 0, 0, 0, nil, err
}
dropped, kept := resolveDroppedKept(matchType, labels, keys)
current, reduced, err = m.ch.EstimateCardinality(ctx, metricName, kept, startMs, endMs)
if err != nil {
return 0, 0, 0, nil, err
}
if current > 0 && reduced <= current {
reductionPercent = (1 - float64(reduced)/float64(current)) * 100
}
return current, reduced, reductionPercent, dropped, nil
}

View File

@@ -0,0 +1,145 @@
package implmetricreductionrule
import (
"context"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/metricreductionruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type store struct {
sqlstore sqlstore.SQLStore
}
func NewStore(sqlstore sqlstore.SQLStore) metricreductionruletypes.Store {
return &store{sqlstore: sqlstore}
}
func (s *store) List(ctx context.Context, orgID valuer.UUID, params *metricreductionruletypes.ListReductionRulesParams) ([]*metricreductionruletypes.ReductionRule, int, error) {
column := "metric_name"
if params.OrderBy == metricreductionruletypes.OrderByLastUpdated {
column = "updated_at"
}
direction := "ASC"
if params.Order == metricreductionruletypes.OrderDesc {
direction = "DESC"
}
rules := make([]*metricreductionruletypes.ReductionRule, 0)
query := s.sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(&rules).
Where("org_id = ?", orgID).
Order(column + " " + direction)
if params.Search != "" {
query = query.Where("metric_name LIKE ?", "%"+params.Search+"%")
}
if params.MetricName != "" {
query = query.Where("metric_name = ?", params.MetricName)
}
if params.Limit > 0 {
query = query.Limit(params.Limit).Offset(params.Offset)
}
total, err := query.ScanAndCount(ctx)
if err != nil {
return nil, 0, err
}
return rules, total, nil
}
func (s *store) Get(ctx context.Context, orgID valuer.UUID, metricName string) (*metricreductionruletypes.ReductionRule, error) {
rule := new(metricreductionruletypes.ReductionRule)
err := s.sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(rule).
Where("org_id = ?", orgID).
Where("metric_name = ?", metricName).
Scan(ctx)
if err != nil {
return nil, s.sqlstore.WrapNotFoundErrf(err, metricreductionruletypes.ErrCodeMetricReductionRuleNotFound, "no reduction rule found for metric %q", metricName)
}
return rule, nil
}
func (s *store) GetByID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*metricreductionruletypes.ReductionRule, error) {
rule := new(metricreductionruletypes.ReductionRule)
err := s.sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(rule).
Where("org_id = ?", orgID).
Where("id = ?", id).
Scan(ctx)
if err != nil {
return nil, s.sqlstore.WrapNotFoundErrf(err, metricreductionruletypes.ErrCodeMetricReductionRuleNotFound, "no reduction rule found with id %q", id.String())
}
return rule, nil
}
func (s *store) Create(ctx context.Context, rule *metricreductionruletypes.ReductionRule) error {
res, err := s.sqlstore.
BunDBCtx(ctx).
NewInsert().
Model(rule).
On("CONFLICT (org_id, metric_name) DO NOTHING").
Exec(ctx)
if err != nil {
return err
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
return errors.Newf(errors.TypeAlreadyExists, metricreductionruletypes.ErrCodeMetricReductionRuleAlreadyExists,
"a reduction rule for metric %q already exists", rule.MetricName)
}
return nil
}
func (s *store) Upsert(ctx context.Context, rule *metricreductionruletypes.ReductionRule) error {
_, err := s.sqlstore.
BunDBCtx(ctx).
NewInsert().
Model(rule).
On("CONFLICT (org_id, metric_name) DO UPDATE").
Set("match_type = EXCLUDED.match_type").
Set("labels = EXCLUDED.labels").
Set("effective_from = EXCLUDED.effective_from").
Set("updated_at = EXCLUDED.updated_at").
Set("updated_by = EXCLUDED.updated_by").
Exec(ctx)
return err
}
func (s *store) DeleteByID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
res, err := s.sqlstore.
BunDBCtx(ctx).
NewDelete().
Model((*metricreductionruletypes.ReductionRule)(nil)).
Where("org_id = ?", orgID).
Where("id = ?", id).
Exec(ctx)
if err != nil {
return err
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
return errors.Newf(errors.TypeNotFound, metricreductionruletypes.ErrCodeMetricReductionRuleNotFound, "no reduction rule found with id %q", id.String())
}
return nil
}
func (s *store) RunInTx(ctx context.Context, cb func(ctx context.Context) error) error {
return s.sqlstore.RunInTxCtx(ctx, nil, cb)
}

View File

@@ -107,6 +107,15 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
Route: "",
})
metricsReduction := ah.Signoz.Flagger.BooleanOrEmpty(ctx, flagger.FeatureEnableMetricsReduction, evalCtx)
featureSet = append(featureSet, &licensetypes.Feature{
Name: valuer.NewString(flagger.FeatureEnableMetricsReduction.String()),
Active: metricsReduction,
Usage: 0,
UsageLimit: -1,
Route: "",
})
if constants.IsDotMetricsEnabled {
for idx, feature := range featureSet {
if feature.Name == licensetypes.DotMetricsEnabled {

View File

@@ -1,13 +0,0 @@
#!/usr/bin/env bash
# Extracts unique fenced code block language identifiers from all .md files under frontend/src/
# Usage: bash frontend/scripts/extract-md-languages.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SRC_DIR="$SCRIPT_DIR/../src"
grep -roh '```[a-zA-Z0-9_+-]*' "$SRC_DIR" --include='*.md' \
| sed 's/^```//' \
| grep -v '^$' \
| sort -u

View File

@@ -1,41 +0,0 @@
#!/usr/bin/env bash
# Validates that all fenced code block languages used in .md files are registered
# in the syntax highlighter.
# Usage: bash frontend/scripts/validate-md-languages.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SYNTAX_HIGHLIGHTER="$SCRIPT_DIR/../src/components/MarkdownRenderer/syntaxHighlighter.ts"
# Get all languages used in .md files
md_languages=$("$SCRIPT_DIR/extract-md-languages.sh")
# Get all registered languages from syntaxHighlighter.ts
registered_languages=$(grep -oP "registerLanguage\('\K[^']+" "$SYNTAX_HIGHLIGHTER" | sort -u)
missing_languages=()
for lang in $md_languages; do
# Skip ai-* block markers — these are custom AI block types rendered by
# RichCodeBlock as React components (e.g. ActionBlock, LineChartBlock),
# not real syntax languages, so they don't need highlighter registration.
if [[ "$lang" == ai-* ]]; then
continue
fi
if ! echo "$registered_languages" | grep -qx "$lang"; then
missing_languages+=("$lang")
fi
done
if [ ${#missing_languages[@]} -gt 0 ]; then
echo "Error: The following languages are used in .md files but not registered in syntaxHighlighter.ts:"
for lang in "${missing_languages[@]}"; do
echo " - $lang"
done
echo ""
echo "Please add them to: frontend/src/components/MarkdownRenderer/syntaxHighlighter.ts"
exit 1
fi
echo "All markdown code block languages are registered in syntaxHighlighter.ts"

View File

@@ -3,7 +3,6 @@ import { matchPath, Redirect, useLocation } from 'react-router-dom';
import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
import { useListUsers } from 'api/generated/services/users';
import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage';
import { ORG_PREFERENCES } from 'constants/orgPreferences';
import ROUTES from 'constants/routes';
@@ -37,7 +36,6 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
activeLicense,
isFetchingActiveLicense,
trialInfo,
featureFlags,
} = useAppContext();
const isAdmin = user.role === USER_ROLES.ADMIN;
@@ -212,14 +210,6 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
}
}
// Check for GET_STARTED → GET_STARTED_WITH_CLOUD redirect (feature flag)
if (
currentRoute?.path === ROUTES.GET_STARTED &&
featureFlags?.find((e) => e.name === FeatureKeys.ONBOARDING_V3)?.active
) {
return <Redirect to={ROUTES.GET_STARTED_WITH_CLOUD} />;
}
// Main routing logic
if (currentRoute) {
const { isPrivate, key } = currentRoute;

View File

@@ -2,7 +2,6 @@ import { ReactElement } from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
import { MemoryRouter, Route, Switch, useLocation } from 'react-router-dom';
import { act, render, screen, waitFor } from '@testing-library/react';
import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage';
import { ORG_PREFERENCES } from 'constants/orgPreferences';
import ROUTES from 'constants/routes';
@@ -1263,80 +1262,6 @@ describe('PrivateRoute', () => {
});
});
describe('Get Started Route Redirect', () => {
it('should redirect to GET_STARTED_WITH_CLOUD when on GET_STARTED and ONBOARDING_V3 feature flag is active', async () => {
renderPrivateRoute({
initialRoute: ROUTES.GET_STARTED,
appContext: {
isLoggedIn: true,
featureFlags: [
{
name: FeatureKeys.ONBOARDING_V3,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
],
},
});
await assertRedirectsTo(ROUTES.GET_STARTED_WITH_CLOUD);
});
it('should not redirect when on GET_STARTED and ONBOARDING_V3 feature flag is inactive', () => {
renderPrivateRoute({
initialRoute: ROUTES.GET_STARTED,
appContext: {
isLoggedIn: true,
featureFlags: [
{
name: FeatureKeys.ONBOARDING_V3,
active: false,
usage: 0,
usage_limit: -1,
route: '',
},
],
},
});
assertStaysOnRoute(ROUTES.GET_STARTED);
});
it('should not redirect when on GET_STARTED and ONBOARDING_V3 feature flag is not present', () => {
renderPrivateRoute({
initialRoute: ROUTES.GET_STARTED,
appContext: {
isLoggedIn: true,
featureFlags: [],
},
});
assertStaysOnRoute(ROUTES.GET_STARTED);
});
it('should not redirect when on different route even if ONBOARDING_V3 is active', () => {
renderPrivateRoute({
initialRoute: ROUTES.HOME,
appContext: {
isLoggedIn: true,
featureFlags: [
{
name: FeatureKeys.ONBOARDING_V3,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
],
},
});
assertStaysOnRoute(ROUTES.HOME);
});
});
describe('Loading States', () => {
it('should not redirect while license is still being fetched', () => {
renderPrivateRoute({
@@ -1496,16 +1421,16 @@ describe('PrivateRoute', () => {
await assertRedirectsTo(ROUTES.UN_AUTHORIZED);
});
it('should allow EDITOR to access /get-started route', () => {
it('should allow EDITOR to access /get-started-with-signoz-cloud route', () => {
renderPrivateRoute({
initialRoute: ROUTES.GET_STARTED,
initialRoute: ROUTES.GET_STARTED_WITH_CLOUD,
appContext: {
isLoggedIn: true,
user: createMockUser({ role: USER_ROLES.EDITOR as ROLES }),
},
});
assertStaysOnRoute(ROUTES.GET_STARTED);
assertStaysOnRoute(ROUTES.GET_STARTED_WITH_CLOUD);
});
});

View File

@@ -90,14 +90,6 @@ export const SettingsPage = Loadable(
() => import(/* webpackChunkName: "SettingsPage" */ 'pages/Settings'),
);
export const GettingStarted = Loadable(
() => import(/* webpackChunkName: "GettingStarted" */ 'pages/GettingStarted'),
);
export const Onboarding = Loadable(
() => import(/* webpackChunkName: "Onboarding" */ 'pages/OnboardingPage'),
);
export const OrgOnboarding = Loadable(
() => import(/* webpackChunkName: "OrgOnboarding" */ 'pages/OrgOnboarding'),
);

View File

@@ -33,7 +33,6 @@ import {
MeterExplorerPage,
MetricsExplorer,
OldLogsExplorer,
Onboarding,
OnboardingV2,
OrgOnboarding,
PasswordReset,
@@ -70,13 +69,6 @@ const routes: AppRoutes[] = [
isPrivate: false,
key: 'SIGN_UP',
},
{
path: ROUTES.GET_STARTED,
exact: false,
component: Onboarding,
isPrivate: true,
key: 'GET_STARTED',
},
{
path: ROUTES.GET_STARTED_WITH_CLOUD,
exact: false,
@@ -477,6 +469,13 @@ const routes: AppRoutes[] = [
key: 'METRICS_EXPLORER_VIEWS',
isPrivate: true,
},
{
path: ROUTES.METRICS_EXPLORER_VOLUME_CONTROL,
exact: true,
component: MetricsExplorer,
key: 'METRICS_EXPLORER_VOLUME_CONTROL',
isPrivate: true,
},
{
path: ROUTES.METER,

View File

@@ -18,6 +18,8 @@ import type {
} from 'react-query';
import type {
CreateMetricReductionRule201,
DeleteMetricReductionRuleByIDPathParameters,
GetMetricAlerts200,
GetMetricAlertsParams,
GetMetricAttributes200,
@@ -28,22 +30,762 @@ import type {
GetMetricHighlightsParams,
GetMetricMetadata200,
GetMetricMetadataParams,
GetMetricReductionRuleByID200,
GetMetricReductionRuleByIDPathParameters,
GetMetricReductionRuleStats200,
GetMetricReductionRuleTimeseries200,
GetMetricsOnboardingStatus200,
GetMetricsStats200,
GetMetricsTreemap200,
InspectMetrics200,
ListMetricReductionRules200,
ListMetricReductionRulesParams,
ListMetrics200,
ListMetricsParams,
MetricreductionruletypesPostableReductionRuleDTO,
MetricreductionruletypesPostableReductionRulePreviewDTO,
MetricreductionruletypesUpdatableReductionRuleDTO,
MetricsexplorertypesInspectMetricsRequestDTO,
MetricsexplorertypesStatsRequestDTO,
MetricsexplorertypesTreemapRequestDTO,
MetricsexplorertypesUpdateMetricMetadataRequestDTO,
PreviewMetricReductionRule200,
RenderErrorResponseDTO,
UpdateMetricReductionRuleByID200,
UpdateMetricReductionRuleByIDPathParameters,
} from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
/**
* Returns active metric volume-control (label reduction) rules.
* @summary List metric reduction rules
*/
export const listMetricReductionRules = (
params?: ListMetricReductionRulesParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ListMetricReductionRules200>({
url: `/api/v2/metric_reduction_rules`,
method: 'GET',
params,
signal,
});
};
export const getListMetricReductionRulesQueryKey = (
params?: ListMetricReductionRulesParams,
) => {
return [
`/api/v2/metric_reduction_rules`,
...(params ? [params] : []),
] as const;
};
export const getListMetricReductionRulesQueryOptions = <
TData = Awaited<ReturnType<typeof listMetricReductionRules>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
params?: ListMetricReductionRulesParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listMetricReductionRules>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getListMetricReductionRulesQueryKey(params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof listMetricReductionRules>>
> = ({ signal }) => listMetricReductionRules(params, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listMetricReductionRules>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListMetricReductionRulesQueryResult = NonNullable<
Awaited<ReturnType<typeof listMetricReductionRules>>
>;
export type ListMetricReductionRulesQueryError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary List metric reduction rules
*/
export function useListMetricReductionRules<
TData = Awaited<ReturnType<typeof listMetricReductionRules>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
params?: ListMetricReductionRulesParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listMetricReductionRules>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListMetricReductionRulesQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary List metric reduction rules
*/
export const invalidateListMetricReductionRules = async (
queryClient: QueryClient,
params?: ListMetricReductionRulesParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListMetricReductionRulesQueryKey(params) },
options,
);
return queryClient;
};
/**
* Creates a volume-control rule for a metric and returns it with its id; fails if the metric already has a rule.
* @summary Create a metric reduction rule
*/
export const createMetricReductionRule = (
metricreductionruletypesPostableReductionRuleDTO?: BodyType<MetricreductionruletypesPostableReductionRuleDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateMetricReductionRule201>({
url: `/api/v2/metric_reduction_rules`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: metricreductionruletypesPostableReductionRuleDTO,
signal,
});
};
export const getCreateMetricReductionRuleMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createMetricReductionRule>>,
TError,
{ data?: BodyType<MetricreductionruletypesPostableReductionRuleDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createMetricReductionRule>>,
TError,
{ data?: BodyType<MetricreductionruletypesPostableReductionRuleDTO> },
TContext
> => {
const mutationKey = ['createMetricReductionRule'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createMetricReductionRule>>,
{ data?: BodyType<MetricreductionruletypesPostableReductionRuleDTO> }
> = (props) => {
const { data } = props ?? {};
return createMetricReductionRule(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateMetricReductionRuleMutationResult = NonNullable<
Awaited<ReturnType<typeof createMetricReductionRule>>
>;
export type CreateMetricReductionRuleMutationBody =
| BodyType<MetricreductionruletypesPostableReductionRuleDTO>
| undefined;
export type CreateMetricReductionRuleMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create a metric reduction rule
*/
export const useCreateMetricReductionRule = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createMetricReductionRule>>,
TError,
{ data?: BodyType<MetricreductionruletypesPostableReductionRuleDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createMetricReductionRule>>,
TError,
{ data?: BodyType<MetricreductionruletypesPostableReductionRuleDTO> },
TContext
> => {
return useMutation(getCreateMetricReductionRuleMutationOptions(options));
};
/**
* Deletes a volume-control rule by its id.
* @summary Delete a metric reduction rule by id
*/
export const deleteMetricReductionRuleByID = (
{ id }: DeleteMetricReductionRuleByIDPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/metric_reduction_rules/${id}`,
method: 'DELETE',
signal,
});
};
export const getDeleteMetricReductionRuleByIDMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteMetricReductionRuleByID>>,
TError,
{ pathParams: DeleteMetricReductionRuleByIDPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteMetricReductionRuleByID>>,
TError,
{ pathParams: DeleteMetricReductionRuleByIDPathParameters },
TContext
> => {
const mutationKey = ['deleteMetricReductionRuleByID'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof deleteMetricReductionRuleByID>>,
{ pathParams: DeleteMetricReductionRuleByIDPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteMetricReductionRuleByID(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteMetricReductionRuleByIDMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteMetricReductionRuleByID>>
>;
export type DeleteMetricReductionRuleByIDMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete a metric reduction rule by id
*/
export const useDeleteMetricReductionRuleByID = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteMetricReductionRuleByID>>,
TError,
{ pathParams: DeleteMetricReductionRuleByIDPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteMetricReductionRuleByID>>,
TError,
{ pathParams: DeleteMetricReductionRuleByIDPathParameters },
TContext
> => {
return useMutation(getDeleteMetricReductionRuleByIDMutationOptions(options));
};
/**
* Returns a single volume-control rule by its id.
* @summary Get a metric reduction rule by id
*/
export const getMetricReductionRuleByID = (
{ id }: GetMetricReductionRuleByIDPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetMetricReductionRuleByID200>({
url: `/api/v2/metric_reduction_rules/${id}`,
method: 'GET',
signal,
});
};
export const getGetMetricReductionRuleByIDQueryKey = ({
id,
}: GetMetricReductionRuleByIDPathParameters) => {
return [`/api/v2/metric_reduction_rules/${id}`] as const;
};
export const getGetMetricReductionRuleByIDQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricReductionRuleByID>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetMetricReductionRuleByIDPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricReductionRuleByID>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetMetricReductionRuleByIDQueryKey({ id });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getMetricReductionRuleByID>>
> = ({ signal }) => getMetricReductionRuleByID({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getMetricReductionRuleByID>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetMetricReductionRuleByIDQueryResult = NonNullable<
Awaited<ReturnType<typeof getMetricReductionRuleByID>>
>;
export type GetMetricReductionRuleByIDQueryError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get a metric reduction rule by id
*/
export function useGetMetricReductionRuleByID<
TData = Awaited<ReturnType<typeof getMetricReductionRuleByID>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetMetricReductionRuleByIDPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricReductionRuleByID>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetMetricReductionRuleByIDQueryOptions(
{ id },
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get a metric reduction rule by id
*/
export const invalidateGetMetricReductionRuleByID = async (
queryClient: QueryClient,
{ id }: GetMetricReductionRuleByIDPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetMetricReductionRuleByIDQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* Updates the match type and labels of a volume-control rule by its id; the metric name is immutable.
* @summary Update a metric reduction rule by id
*/
export const updateMetricReductionRuleByID = (
{ id }: UpdateMetricReductionRuleByIDPathParameters,
metricreductionruletypesUpdatableReductionRuleDTO?: BodyType<MetricreductionruletypesUpdatableReductionRuleDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<UpdateMetricReductionRuleByID200>({
url: `/api/v2/metric_reduction_rules/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: metricreductionruletypesUpdatableReductionRuleDTO,
signal,
});
};
export const getUpdateMetricReductionRuleByIDMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateMetricReductionRuleByID>>,
TError,
{
pathParams: UpdateMetricReductionRuleByIDPathParameters;
data?: BodyType<MetricreductionruletypesUpdatableReductionRuleDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateMetricReductionRuleByID>>,
TError,
{
pathParams: UpdateMetricReductionRuleByIDPathParameters;
data?: BodyType<MetricreductionruletypesUpdatableReductionRuleDTO>;
},
TContext
> => {
const mutationKey = ['updateMetricReductionRuleByID'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof updateMetricReductionRuleByID>>,
{
pathParams: UpdateMetricReductionRuleByIDPathParameters;
data?: BodyType<MetricreductionruletypesUpdatableReductionRuleDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateMetricReductionRuleByID(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateMetricReductionRuleByIDMutationResult = NonNullable<
Awaited<ReturnType<typeof updateMetricReductionRuleByID>>
>;
export type UpdateMetricReductionRuleByIDMutationBody =
| BodyType<MetricreductionruletypesUpdatableReductionRuleDTO>
| undefined;
export type UpdateMetricReductionRuleByIDMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update a metric reduction rule by id
*/
export const useUpdateMetricReductionRuleByID = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateMetricReductionRuleByID>>,
TError,
{
pathParams: UpdateMetricReductionRuleByIDPathParameters;
data?: BodyType<MetricreductionruletypesUpdatableReductionRuleDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateMetricReductionRuleByID>>,
TError,
{
pathParams: UpdateMetricReductionRuleByIDPathParameters;
data?: BodyType<MetricreductionruletypesUpdatableReductionRuleDTO>;
},
TContext
> => {
return useMutation(getUpdateMetricReductionRuleByIDMutationOptions(options));
};
/**
* Estimates the series reduction and related-asset impact of a candidate volume-control rule without persisting it.
* @summary Preview a metric reduction rule
*/
export const previewMetricReductionRule = (
metricreductionruletypesPostableReductionRulePreviewDTO?: BodyType<MetricreductionruletypesPostableReductionRulePreviewDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<PreviewMetricReductionRule200>({
url: `/api/v2/metric_reduction_rules/preview`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: metricreductionruletypesPostableReductionRulePreviewDTO,
signal,
});
};
export const getPreviewMetricReductionRuleMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof previewMetricReductionRule>>,
TError,
{ data?: BodyType<MetricreductionruletypesPostableReductionRulePreviewDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof previewMetricReductionRule>>,
TError,
{ data?: BodyType<MetricreductionruletypesPostableReductionRulePreviewDTO> },
TContext
> => {
const mutationKey = ['previewMetricReductionRule'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof previewMetricReductionRule>>,
{ data?: BodyType<MetricreductionruletypesPostableReductionRulePreviewDTO> }
> = (props) => {
const { data } = props ?? {};
return previewMetricReductionRule(data);
};
return { mutationFn, ...mutationOptions };
};
export type PreviewMetricReductionRuleMutationResult = NonNullable<
Awaited<ReturnType<typeof previewMetricReductionRule>>
>;
export type PreviewMetricReductionRuleMutationBody =
| BodyType<MetricreductionruletypesPostableReductionRulePreviewDTO>
| undefined;
export type PreviewMetricReductionRuleMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Preview a metric reduction rule
*/
export const usePreviewMetricReductionRule = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof previewMetricReductionRule>>,
TError,
{ data?: BodyType<MetricreductionruletypesPostableReductionRulePreviewDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof previewMetricReductionRule>>,
TError,
{ data?: BodyType<MetricreductionruletypesPostableReductionRulePreviewDTO> },
TContext
> => {
return useMutation(getPreviewMetricReductionRuleMutationOptions(options));
};
/**
* Returns total ingested vs retained series and the estimated monthly savings across all volume-control rules.
* @summary Metric reduction stats
*/
export const getMetricReductionRuleStats = (signal?: AbortSignal) => {
return GeneratedAPIInstance<GetMetricReductionRuleStats200>({
url: `/api/v2/metric_reduction_rules/stats`,
method: 'GET',
signal,
});
};
export const getGetMetricReductionRuleStatsQueryKey = () => {
return [`/api/v2/metric_reduction_rules/stats`] as const;
};
export const getGetMetricReductionRuleStatsQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricReductionRuleStats>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricReductionRuleStats>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetMetricReductionRuleStatsQueryKey();
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getMetricReductionRuleStats>>
> = ({ signal }) => getMetricReductionRuleStats(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getMetricReductionRuleStats>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetMetricReductionRuleStatsQueryResult = NonNullable<
Awaited<ReturnType<typeof getMetricReductionRuleStats>>
>;
export type GetMetricReductionRuleStatsQueryError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Metric reduction stats
*/
export function useGetMetricReductionRuleStats<
TData = Awaited<ReturnType<typeof getMetricReductionRuleStats>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricReductionRuleStats>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetMetricReductionRuleStatsQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Metric reduction stats
*/
export const invalidateGetMetricReductionRuleStats = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetMetricReductionRuleStatsQueryKey() },
options,
);
return queryClient;
};
/**
* Returns ingested vs retained series over time across all volume-control rules (hourly buckets), in the query-range time-series response shape.
* @summary Metric reduction volume over time
*/
export const getMetricReductionRuleTimeseries = (signal?: AbortSignal) => {
return GeneratedAPIInstance<GetMetricReductionRuleTimeseries200>({
url: `/api/v2/metric_reduction_rules/timeseries`,
method: 'GET',
signal,
});
};
export const getGetMetricReductionRuleTimeseriesQueryKey = () => {
return [`/api/v2/metric_reduction_rules/timeseries`] as const;
};
export const getGetMetricReductionRuleTimeseriesQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricReductionRuleTimeseries>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricReductionRuleTimeseries>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetMetricReductionRuleTimeseriesQueryKey();
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getMetricReductionRuleTimeseries>>
> = ({ signal }) => getMetricReductionRuleTimeseries(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getMetricReductionRuleTimeseries>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetMetricReductionRuleTimeseriesQueryResult = NonNullable<
Awaited<ReturnType<typeof getMetricReductionRuleTimeseries>>
>;
export type GetMetricReductionRuleTimeseriesQueryError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Metric reduction volume over time
*/
export function useGetMetricReductionRuleTimeseries<
TData = Awaited<ReturnType<typeof getMetricReductionRuleTimeseries>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricReductionRuleTimeseries>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetMetricReductionRuleTimeseriesQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Metric reduction volume over time
*/
export const invalidateGetMetricReductionRuleTimeseries = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetMetricReductionRuleTimeseriesQueryKey() },
options,
);
return queryClient;
};
/**
* This endpoint returns a list of distinct metric names within the specified time range
* @summary List metric names

View File

@@ -6794,6 +6794,213 @@ export interface LlmpricingruletypesUpdatableLLMPricingRulesDTO {
rules: LlmpricingruletypesUpdatableLLMPricingRuleDTO[] | null;
}
export enum MetricreductionruletypesAssetTypeDTO {
dashboard = 'dashboard',
alert_rule = 'alert_rule',
}
export interface MetricreductionruletypesAffectedWidgetDTO {
/**
* @type string
*/
id: string;
/**
* @type string
*/
name: string;
}
export interface MetricreductionruletypesAffectedAssetDTO {
/**
* @type string
*/
id: string;
/**
* @type array,null
*/
impactedLabels: string[] | null;
/**
* @type string
*/
name: string;
type: MetricreductionruletypesAssetTypeDTO;
widget?: MetricreductionruletypesAffectedWidgetDTO;
}
export enum MetricreductionruletypesMatchTypeDTO {
drop = 'drop',
keep = 'keep',
}
export interface MetricreductionruletypesGettableReductionRuleDTO {
/**
* @type boolean
*/
active: boolean;
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @type string
*/
createdBy?: string;
/**
* @type string
* @format date-time
*/
effectiveFrom: string;
/**
* @type string
*/
id: string;
/**
* @type integer
* @minimum 0
*/
ingestedSeries: number;
/**
* @type array,null
*/
labels: string[] | null;
matchType: MetricreductionruletypesMatchTypeDTO;
/**
* @type string
*/
metricName: string;
/**
* @type number
* @format double
*/
reductionPercent: number;
/**
* @type integer
* @minimum 0
*/
retainedSeries: number;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
/**
* @type string
*/
updatedBy?: string;
}
export interface MetricreductionruletypesGettableReductionRulePreviewDTO {
/**
* @type array,null
*/
affectedAssets: MetricreductionruletypesAffectedAssetDTO[] | null;
/**
* @type integer
* @minimum 0
*/
currentRetainedSeries: number;
/**
* @type array,null
*/
droppedLabels: string[] | null;
/**
* @type string
* @format date-time
*/
effectiveFrom: string;
/**
* @type integer
* @minimum 0
*/
ingestedSeries: number;
/**
* @type number
* @format double
*/
reductionPercent: number;
/**
* @type integer
* @minimum 0
*/
retainedSeries: number;
}
export interface MetricreductionruletypesGettableReductionRuleStatsDTO {
/**
* @type number
* @format double
*/
estimatedMonthlySavingsUsd: number;
/**
* @type integer
* @minimum 0
*/
ingestedSeries: number;
/**
* @type integer
* @minimum 0
*/
retainedSeries: number;
}
export interface MetricreductionruletypesGettableReductionRulesDTO {
/**
* @type array,null
*/
rules: MetricreductionruletypesGettableReductionRuleDTO[] | null;
/**
* @type integer
*/
total: number;
}
export enum MetricreductionruletypesOrderDTO {
asc = 'asc',
desc = 'desc',
}
export interface MetricreductionruletypesPostableReductionRuleDTO {
/**
* @type array,null
*/
labels: string[] | null;
matchType: MetricreductionruletypesMatchTypeDTO;
/**
* @type string
*/
metricName: string;
}
export interface MetricreductionruletypesPostableReductionRulePreviewDTO {
/**
* @type array,null
*/
labels: string[] | null;
/**
* @type integer
* @format int64
*/
lookbackMs?: number;
matchType: MetricreductionruletypesMatchTypeDTO;
/**
* @type string
*/
metricName: string;
}
export enum MetricreductionruletypesReductionRuleOrderByDTO {
metric = 'metric',
ingested_volume = 'ingested_volume',
reduced_volume = 'reduced_volume',
reduction = 'reduction',
last_updated = 'last_updated',
}
export interface MetricreductionruletypesUpdatableReductionRuleDTO {
/**
* @type array,null
*/
labels: string[] | null;
matchType: MetricreductionruletypesMatchTypeDTO;
}
export interface MetricsexplorertypesInspectMetricsRequestDTO {
/**
* @type integer
@@ -10474,6 +10681,102 @@ export type Livez200 = {
status: string;
};
export type ListMetricReductionRulesParams = {
/**
* @description undefined
*/
orderBy?: MetricreductionruletypesReductionRuleOrderByDTO;
/**
* @description undefined
*/
order?: MetricreductionruletypesOrderDTO;
/**
* @type string
* @description undefined
*/
search?: string;
/**
* @type string
* @description undefined
*/
metricName?: string;
/**
* @type integer
* @description undefined
*/
offset?: number;
/**
* @type integer
* @description undefined
*/
limit?: number;
};
export type ListMetricReductionRules200 = {
data: MetricreductionruletypesGettableReductionRulesDTO;
/**
* @type string
*/
status: string;
};
export type CreateMetricReductionRule201 = {
data: MetricreductionruletypesGettableReductionRuleDTO;
/**
* @type string
*/
status: string;
};
export type DeleteMetricReductionRuleByIDPathParameters = {
id: string;
};
export type GetMetricReductionRuleByIDPathParameters = {
id: string;
};
export type GetMetricReductionRuleByID200 = {
data: MetricreductionruletypesGettableReductionRuleDTO;
/**
* @type string
*/
status: string;
};
export type UpdateMetricReductionRuleByIDPathParameters = {
id: string;
};
export type UpdateMetricReductionRuleByID200 = {
data: MetricreductionruletypesGettableReductionRuleDTO;
/**
* @type string
*/
status: string;
};
export type PreviewMetricReductionRule200 = {
data: MetricreductionruletypesGettableReductionRulePreviewDTO;
/**
* @type string
*/
status: string;
};
export type GetMetricReductionRuleStats200 = {
data: MetricreductionruletypesGettableReductionRuleStatsDTO;
/**
* @type string
*/
status: string;
};
export type GetMetricReductionRuleTimeseries200 = {
data: Querybuildertypesv5QueryRangeResponseDTO;
/**
* @type string
*/
status: string;
};
export type ListMetricsParams = {
/**
* @type integer,null

View File

@@ -62,13 +62,13 @@ function ErrorTitleAndKey({
switch (parentTitle) {
case 'Consumers':
link = `${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Consumers}`;
link = `${ROUTES.GET_STARTED_WITH_CLOUD}?${QueryParams.getStartedSource}=self-hosted-kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Consumers}`;
break;
case 'Producers':
link = `${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Producers}`;
link = `${ROUTES.GET_STARTED_WITH_CLOUD}?${QueryParams.getStartedSource}=self-hosted-kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Producers}`;
break;
case 'Kafka':
link = `${ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Kafka}`;
link = `${ROUTES.GET_STARTED_WITH_CLOUD}?${QueryParams.getStartedSource}=self-hosted-kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Kafka}`;
break;
default:
link = '';

View File

@@ -7,10 +7,10 @@ export enum FeatureKeys {
GATEWAY = 'gateway',
PREMIUM_SUPPORT = 'premium_support',
ANOMALY_DETECTION = 'anomaly_detection',
ONBOARDING_V3 = 'onboarding_v3',
DOT_METRICS_ENABLED = 'dot_metrics_enabled',
USE_JSON_BODY = 'use_json_body',
USE_FINE_GRAINED_AUTHZ = 'use_fine_grained_authz',
USE_DASHBOARD_V2 = 'use_dashboard_v2',
EMABLE_AI_OBSERVABILITY = 'enable_ai_observability',
ENABLE_METRICS_REDUCTION = 'enable_metrics_reduction',
}

View File

@@ -11,14 +11,7 @@ const ROUTES = {
TRACE_DETAIL_OLD: '/trace-old/:id',
TRACES_EXPLORER: '/traces-explorer',
ONBOARDING: '/onboarding',
GET_STARTED: '/get-started',
GET_STARTED_WITH_CLOUD: '/get-started-with-signoz-cloud',
GET_STARTED_APPLICATION_MONITORING: '/get-started/application-monitoring',
GET_STARTED_LOGS_MANAGEMENT: '/get-started/logs-management',
GET_STARTED_INFRASTRUCTURE_MONITORING:
'/get-started/infrastructure-monitoring',
GET_STARTED_AWS_MONITORING: '/get-started/aws-monitoring',
GET_STARTED_AZURE_MONITORING: '/get-started/azure-monitoring',
USAGE_EXPLORER: '/usage-explorer',
APPLICATION: '/services',
ALL_DASHBOARD: '/dashboard',
@@ -81,6 +74,7 @@ const ROUTES = {
METRICS_EXPLORER: '/metrics-explorer/summary',
METRICS_EXPLORER_EXPLORER: '/metrics-explorer/explorer',
METRICS_EXPLORER_VIEWS: '/metrics-explorer/views',
METRICS_EXPLORER_VOLUME_CONTROL: '/metrics-explorer/volume-control',
API_MONITORING_BASE: '/api-monitoring',
API_MONITORING: '/api-monitoring/explorer',
METRICS_EXPLORER_BASE: '/metrics-explorer',

View File

@@ -413,14 +413,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const isPanelEditorV2 = routeKey === 'DASHBOARD_PANEL_EDITOR';
const renderFullScreen =
pathname === ROUTES.GET_STARTED ||
pathname === ROUTES.ONBOARDING ||
pathname === ROUTES.GET_STARTED_WITH_CLOUD ||
pathname === ROUTES.GET_STARTED_APPLICATION_MONITORING ||
pathname === ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING ||
pathname === ROUTES.GET_STARTED_LOGS_MANAGEMENT ||
pathname === ROUTES.GET_STARTED_AWS_MONITORING ||
pathname === ROUTES.GET_STARTED_AZURE_MONITORING ||
isPublicDashboard ||
isPanelEditorV2;

View File

@@ -1,29 +0,0 @@
.full-screen-header-container {
display: flex;
justify-content: center;
align-items: center;
padding: 24px 0;
.brand-logo {
display: flex;
justify-content: center;
align-items: center;
gap: 16px;
cursor: pointer;
img {
height: 32px;
width: 32px;
}
.brand-logo-name {
font-family: 'Work Sans', sans-serif;
font-size: 24px;
font-style: normal;
font-weight: 500;
line-height: 18px;
color: var(--l1-foreground);
}
}
}

View File

@@ -1,28 +0,0 @@
import history from 'lib/history';
import signozBrandLogoUrl from '@/assets/Logos/signoz-brand-logo.svg';
import './FullScreenHeader.styles.scss';
export default function FullScreenHeader({
overrideRoute,
}: {
overrideRoute?: string;
}): React.ReactElement {
const handleLogoClick = (): void => {
history.push(overrideRoute || '/');
};
return (
<div className="full-screen-header-container">
<div className="brand-logo" onClick={handleLogoClick}>
<img src={signozBrandLogoUrl} alt="SigNoz" />
<div className="brand-logo-name">SigNoz</div>
</div>
</div>
);
}
FullScreenHeader.defaultProps = {
overrideRoute: '/',
};

View File

@@ -9,6 +9,7 @@ import EditMemberDrawer from 'components/EditMemberDrawer/EditMemberDrawer';
import InviteMembersModal from 'components/InviteMembersModal/InviteMembersModal';
import MembersTable, { MemberRow } from 'components/MembersTable/MembersTable';
import useUrlQuery from 'hooks/useUrlQuery';
import { parseAsBoolean, useQueryState } from 'nuqs';
import { toISOString } from 'utils/app';
import { FilterMode, MemberStatus, toMemberStatus } from './utils';
@@ -26,7 +27,10 @@ function MembersSettings(): JSX.Element {
// TODO(nuqs): Replace with nuqs once the nuqs setup and integration is done - for search
const [searchQuery, setSearchQuery] = useState('');
const [filterMode, setFilterMode] = useState<FilterMode>(FilterMode.All);
const [isInviteModalOpen, setIsInviteModalOpen] = useState(false);
const [isInviteModalOpen, setIsInviteModalOpen] = useQueryState(
'invite',
parseAsBoolean.withDefault(false),
);
const [selectedMember, setSelectedMember] = useState<MemberRow | null>(null);
const { data: usersData, isLoading, refetch: refetchUsers } = useListUsers();
@@ -201,7 +205,7 @@ function MembersSettings(): JSX.Element {
<Button
variant="solid"
color="primary"
onClick={(): void => setIsInviteModalOpen(true)}
onClick={(): void => void setIsInviteModalOpen(true)}
>
<Plus size={12} />
Invite member
@@ -221,7 +225,7 @@ function MembersSettings(): JSX.Element {
<InviteMembersModal
open={isInviteModalOpen}
onClose={(): void => setIsInviteModalOpen(false)}
onClose={(): void => void setIsInviteModalOpen(null)}
onComplete={handleInviteComplete}
/>

View File

@@ -130,4 +130,14 @@ describe('MembersSettings (integration)', () => {
screen.findAllByPlaceholderText('john@signoz.io'),
).resolves.toHaveLength(3);
});
it('opens InviteMembersModal when invite=true query param is present', async () => {
render(<MembersSettings />, undefined, {
initialRoute: '/settings/members?invite=true',
});
await expect(
screen.findAllByPlaceholderText('john@signoz.io'),
).resolves.toHaveLength(3);
});
});

View File

@@ -21,6 +21,7 @@ import AllAttributes from './AllAttributes';
import DashboardsAndAlertsPopover from './DashboardsAndAlertsPopover';
import Highlights from './Highlights';
import Metadata from './Metadata';
import VolumeControlSection from '../VolumeControl/components/VolumeControlSection/VolumeControlSection';
import { MetricDetailsProps } from './types';
import { getMetricDetailsQuery } from './utils';
@@ -190,6 +191,7 @@ function MetricDetails({
isLoadingMetricMetadata={isLoadingMetricMetadata}
refetchMetricMetadata={refetchMetricMetadata}
/>
<VolumeControlSection metricName={metricName} />
<AllAttributes
metricName={metricName}
metricType={metadata?.type}

View File

@@ -77,6 +77,14 @@ jest.mock(
},
);
jest.mock(
'container/MetricsExplorer/VolumeControl/components/VolumeControlSection/VolumeControlSection',
() =>
function MockVolumeControlSection(): JSX.Element {
return <div data-testid="volume-control-section-mock">Volume Control</div>;
},
);
const useGetMetricMetadataMock = jest.spyOn(
metricsExplorerHooks,
'useGetMetricMetadata',

View File

@@ -58,6 +58,7 @@
}
.metrics-table-container {
padding-bottom: 48px;
.ant-table {
margin-left: -16px;
margin-right: -16px;

View File

@@ -0,0 +1,64 @@
import {
Querybuildertypesv5QueryRangeResponseDTO,
Querybuildertypesv5TimeSeriesDataDTO,
Querybuildertypesv5TimeSeriesDTO,
Querybuildertypesv5TimeSeriesValueDTO,
} from 'api/generated/services/sigNoz.schemas';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
function findSeries(
series: Querybuildertypesv5TimeSeriesDTO[] | null | undefined,
label: string,
): Querybuildertypesv5TimeSeriesDTO | undefined {
return series?.find((entry) =>
(entry.labels ?? []).some((value) => value.value === label),
);
}
function toChartValues(
points: Querybuildertypesv5TimeSeriesValueDTO[],
): [number, string][] {
return points.map((point) => [
Math.floor((point.timestamp ?? 0) / 1000),
String(point.value ?? 0),
]);
}
export function buildVolumeChartPayload(
response?: Querybuildertypesv5QueryRangeResponseDTO,
): SuccessResponse<MetricRangePayloadProps> {
const result = response?.data?.results?.[0] as
| Querybuildertypesv5TimeSeriesDataDTO
| undefined;
const series = result?.aggregations?.[0]?.series;
const ingested = findSeries(series, 'ingested')?.values ?? [];
const retained = findSeries(series, 'retained')?.values ?? [];
return {
statusCode: 200,
message: 'Success',
error: null,
payload: {
data: {
resultType: 'matrix',
result: [
{
queryName: 'ingested',
legend: 'Ingested',
metric: {},
values: toChartValues(ingested),
},
{
queryName: 'retained',
legend: 'Retained',
metric: {},
values: toChartValues(retained),
},
],
newResult: { data: { result: [], resultType: 'matrix' } },
},
},
};
}

View File

@@ -0,0 +1,23 @@
import { Gauge } from '@signozhq/icons';
import { MetricreductionruletypesGettableReductionRuleDTO } from 'api/generated/services/sigNoz.schemas';
import { Badge } from '@signozhq/ui/badge';
interface VolumeControlBadgeProps {
rule: MetricreductionruletypesGettableReductionRuleDTO;
}
function VolumeControlBadge({ rule }: VolumeControlBadgeProps): JSX.Element {
return (
<Badge
data-testid="vc-badge-active"
variant="outline"
color={!rule.active ? 'success' : 'warning'}
>
<Gauge size={12} />
{!rule.active ? 'Active' : 'Pending'}
</Badge>
);
}
export default VolumeControlBadge;

View File

@@ -0,0 +1,17 @@
.chart {
display: flex;
flex-direction: column;
gap: 8px;
border: 1px solid var(--bg-slate-400, #1d212d);
border-radius: 6px;
padding: 12px 16px 0 16px;
}
.chartTitle {
text-transform: uppercase;
letter-spacing: 0.05em;
}
.chartBody {
height: 340px;
}

View File

@@ -0,0 +1,88 @@
import { Color } from '@signozhq/design-tokens';
import { Typography } from '@signozhq/ui/typography';
import { useGetMetricReductionRuleTimeseries } from 'api/generated/services/metrics';
import { PANEL_TYPES } from 'constants/queryBuilder';
import BarChart from 'container/DashboardContainer/visualization/charts/BarChart/BarChart';
import { buildBaseConfig } from 'container/DashboardContainer/visualization/panels/utils/baseConfigBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { LegendPosition } from 'lib/uPlotV2/components/types';
import { DrawStyle } from 'lib/uPlotV2/config/types';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useTimezone } from 'providers/Timezone';
import { useMemo, useRef } from 'react';
import { buildVolumeChartPayload } from '../../chartUtils';
import styles from './VolumeControlChart.module.scss';
const COLOR_MAPPING: Record<string, string> = {
Ingested: Color.BG_ROBIN_500,
Retained: Color.BG_FOREST_500,
};
interface VolumeControlChartProps {
enabled: boolean;
}
function VolumeControlChart({ enabled }: VolumeControlChartProps): JSX.Element {
const { data } = useGetMetricReductionRuleTimeseries({ query: { enabled } });
const isDarkMode = useIsDarkMode();
const { timezone } = useTimezone();
const graphRef = useRef<HTMLDivElement>(null);
const dimensions = useResizeObserver(graphRef);
const payload = useMemo(
() => buildVolumeChartPayload(data?.data).payload,
[data],
);
const chartData = useMemo(() => getUPlotChartData(payload), [payload]);
const config = useMemo(() => {
const timestamps = (chartData[0] as number[]) ?? [];
const builder = buildBaseConfig({
id: 'metric-volume-control',
isDarkMode,
apiResponse: payload,
timezone,
panelType: PANEL_TYPES.BAR,
yAxisUnit: 'short',
onDragSelect: (): void => {},
minTimeScale: timestamps[0],
maxTimeScale: timestamps[timestamps.length - 1],
});
(payload.data.result ?? []).forEach((series) => {
builder.addSeries({
scaleKey: 'y',
drawStyle: DrawStyle.Bar,
label: series.legend ?? series.queryName,
colorMapping: COLOR_MAPPING,
isDarkMode,
});
});
return builder;
}, [payload, chartData, isDarkMode, timezone]);
return (
<div className={styles.chart} data-testid="volume-control-chart">
<Typography.Text className={styles.chartTitle} size={'small'}>
Series volume over time · ingested vs retained
</Typography.Text>
<div className={styles.chartBody} ref={graphRef}>
{dimensions.width > 0 && (
<BarChart
config={config}
data={chartData}
width={dimensions.width}
height={dimensions.height}
yAxisUnit="short"
timezone={timezone}
legendConfig={{ position: LegendPosition.BOTTOM }}
/>
)}
</div>
</div>
);
}
export default VolumeControlChart;

View File

@@ -0,0 +1,27 @@
.impactPanel {
border: 1px solid var(--bg-slate-400, #1d212d);
border-radius: 6px;
background: var(--bg-ink-300, #16181d);
padding: 14px;
}
.meterGrid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.meter {
display: flex;
flex-direction: column;
gap: 5px;
}
.meterLabel {
text-transform: uppercase;
letter-spacing: 0.04em;
}
.meterValue {
font-family: 'Geist Mono', monospace;
}

View File

@@ -0,0 +1,81 @@
import { Typography } from '@signozhq/ui/typography';
import { Spin } from 'antd';
import { MetricreductionruletypesGettableReductionRulePreviewDTO } from 'api/generated/services/sigNoz.schemas';
import { formatCompact } from '../../../configUtils';
import { RuleMode } from '../../../types';
import styles from './ImpactPanel.module.scss';
interface ImpactPanelProps {
mode: RuleMode;
preview?: MetricreductionruletypesGettableReductionRulePreviewDTO;
isLoading: boolean;
}
function ImpactPanel({
mode,
preview,
isLoading,
}: ImpactPanelProps): JSX.Element {
if (mode === 'all') {
return (
<div className={styles.impactPanel} data-testid="volume-control-impact">
<Typography.Text size="small" color="muted">
All attributes remain queryable, no reduction.
</Typography.Text>
</div>
);
}
const current = preview?.currentRetainedSeries ?? 0;
const proposed = preview?.retainedSeries ?? 0;
const deltaPct = current > 0 ? (1 - proposed / current) * 100 : 0;
const reductionLabel = `${deltaPct >= 0 ? '' : '+'}${Math.round(
Math.abs(deltaPct),
)}%`;
return (
<div className={styles.impactPanel} data-testid="volume-control-impact">
{isLoading && <Spin size="small" />}
{!isLoading && preview && (
<div className={styles.meterGrid}>
<div className={styles.meter}>
<Typography.Text size="xs" color="muted" className={styles.meterLabel}>
Current series
</Typography.Text>
<Typography.Text size="2xl" className={styles.meterValue}>
{formatCompact(current)}
</Typography.Text>
</div>
<div className={styles.meter}>
<Typography.Text size="xs" color="muted" className={styles.meterLabel}>
Proposed series
</Typography.Text>
<Typography.Text size="2xl" className={styles.meterValue}>
{formatCompact(proposed)}
</Typography.Text>
</div>
<div className={styles.meter}>
<Typography.Text size="xs" color="muted" className={styles.meterLabel}>
Reduction
</Typography.Text>
<Typography.Text
size="2xl"
color={deltaPct >= 0 ? 'success' : undefined}
className={styles.meterValue}
>
{reductionLabel}
</Typography.Text>
</div>
</div>
)}
{!isLoading && !preview && (
<Typography.Text size="small" color="muted">
Select attributes to preview the impact.
</Typography.Text>
)}
</div>
);
}
export default ImpactPanel;

View File

@@ -0,0 +1,10 @@
.fieldGroup {
display: flex;
flex-direction: column;
gap: 6px;
}
.attributeSelect {
width: 100%;
margin-top: 4px;
}

View File

@@ -0,0 +1,54 @@
import { Typography } from '@signozhq/ui/typography';
import { Select } from 'antd';
import { popupContainer } from 'utils/selectPopupContainer';
import { RuleMode } from '../../../types';
import styles from './LabelSelector.module.scss';
interface LabelSelectorProps {
mode: RuleMode;
options: string[];
value: string[];
onChange: (labels: string[]) => void;
loading?: boolean;
}
function LabelSelector({
mode,
options,
value,
onChange,
loading,
}: LabelSelectorProps): JSX.Element {
const helpText =
mode === 'include'
? 'Only the selected attributes will remain queryable.'
: 'The selected attributes will be aggregated away; all others stay queryable.';
return (
<div
className={styles.fieldGroup}
data-testid="volume-control-label-selector"
>
<Typography.Text size="small" weight="semibold">
Attributes
</Typography.Text>
<Typography.Text size="sm" color="muted">
{helpText}
</Typography.Text>
<Select
mode="multiple"
className={styles.attributeSelect}
placeholder="Select attributes"
value={value}
onChange={onChange}
loading={loading}
options={options.map((key) => ({ label: key, value: key }))}
getPopupContainer={popupContainer}
data-testid="volume-control-label-select"
/>
</div>
);
}
export default LabelSelector;

View File

@@ -0,0 +1,29 @@
.modeOptions {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.modeOption {
display: flex;
flex-direction: column;
gap: 4px;
text-align: left;
border: 1px solid var(--bg-slate-400, #1d212d);
border-radius: 6px;
background: var(--bg-ink-300, #16181d);
padding: 12px;
cursor: pointer;
transition:
border-color 0.12s ease,
background 0.12s ease;
&:hover {
border-color: var(--bg-slate-200, #2c3140);
}
}
.modeOptionSelected {
border-color: var(--bg-robin-500, #4e74f8);
background: var(--callout-primary-background);
}

View File

@@ -0,0 +1,64 @@
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import { RuleMode } from '../../../types';
import styles from './ModeSelector.module.scss';
interface ModeOption {
mode: RuleMode;
title: string;
description: string;
}
const MODE_OPTIONS: ModeOption[] = [
{
mode: 'all',
title: 'Allow all attributes',
description: 'All attributes stay queryable. Removes any existing rule.',
},
{
mode: 'include',
title: 'Include attributes',
description: 'Allowlist: only the selected attributes stay queryable.',
},
{
mode: 'exclude',
title: 'Exclude attributes',
description: 'Blocklist: the selected attributes are aggregated away.',
},
];
interface ModeSelectorProps {
mode: RuleMode;
onChange: (mode: RuleMode) => void;
}
function ModeSelector({ mode, onChange }: ModeSelectorProps): JSX.Element {
return (
<div
className={styles.modeOptions}
data-testid="volume-control-mode-selector"
>
{MODE_OPTIONS.map((option) => (
<button
type="button"
key={option.mode}
className={cx(styles.modeOption, {
[styles.modeOptionSelected]: mode === option.mode,
})}
onClick={(): void => onChange(option.mode)}
data-testid={`volume-control-mode-${option.mode}`}
>
<Typography.Text size="small" weight="semibold">
{option.title}
</Typography.Text>
<Typography.Text size="sm" color="muted">
{option.description}
</Typography.Text>
</button>
))}
</div>
);
}
export default ModeSelector;

View File

@@ -0,0 +1,20 @@
.warning {
display: flex;
gap: 10px;
padding: 11px 13px;
border-radius: 6px;
background: var(--callout-warning-background);
border: 1px solid var(--callout-warning-border);
color: var(--callout-warning-icon);
}
.warningBody {
display: flex;
flex-direction: column;
gap: 4px;
}
.assetList {
margin: 2px 0 0;
padding-left: 16px;
}

View File

@@ -0,0 +1,97 @@
import { Info } from '@signozhq/icons';
import { Typography } from '@signozhq/ui/typography';
import {
MetricreductionruletypesAffectedAssetDTO,
MetricreductionruletypesAssetTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import styles from './RelatedAssetsWarning.module.scss';
const AssetType = MetricreductionruletypesAssetTypeDTO;
function assetHref(
asset: MetricreductionruletypesAffectedAssetDTO,
): string | undefined {
if (!asset.id) {
return undefined;
}
if (asset.type === AssetType.dashboard) {
const base = ROUTES.DASHBOARD.replace(':dashboardId', asset.id);
return asset.widget?.id
? `${base}?${QueryParams.expandedWidgetId}=${asset.widget.id}`
: base;
}
if (asset.type === AssetType.alert_rule) {
return `${ROUTES.EDIT_ALERTS}?ruleId=${asset.id}`;
}
return undefined;
}
interface RelatedAssetsWarningProps {
affectedAssets?: MetricreductionruletypesAffectedAssetDTO[] | null;
}
function RelatedAssetsWarning({
affectedAssets,
}: RelatedAssetsWarningProps): JSX.Element | null {
const impacted = (affectedAssets ?? []).filter(
(asset) =>
asset.type === AssetType.alert_rule ||
(asset.impactedLabels ?? []).length > 0,
);
if (impacted.length === 0) {
return null;
}
const impactedLabels = Array.from(
new Set(impacted.flatMap((asset) => asset.impactedLabels ?? [])),
);
return (
<div className={styles.warning} data-testid="volume-control-warning">
<Info size={14} />
<div className={styles.warningBody}>
<Typography.Text as="div" size="small" weight="semibold" color="warning">
This rule affects {impacted.length} related asset
{impacted.length > 1 ? 's' : ''}.
</Typography.Text>
{impactedLabels.length > 0 && (
<Typography.Text as="div" size="sm" color="muted">
{impactedLabels.join(', ')} will no longer be queryable; affected panels
fall back to aggregated data once the rule applies.
</Typography.Text>
)}
<ul className={styles.assetList}>
{impacted.map((asset) => {
const href = assetHref(asset);
const label = `${asset.name}${
asset.widget ? ` · ${asset.widget.name}` : ''
}`;
return (
<li key={`${asset.type}-${asset.id}-${asset.widget?.id ?? ''}`}>
{href ? (
<Typography.Link
size="sm"
href={href}
target="_blank"
rel="noopener noreferrer"
>
{label}
</Typography.Link>
) : (
<Typography.Text size="sm" color="muted">
{label}
</Typography.Text>
)}
</li>
);
})}
</ul>
</div>
</div>
);
}
export default RelatedAssetsWarning;

View File

@@ -0,0 +1,30 @@
.body {
display: flex;
flex-direction: column;
gap: 20px;
padding: 4px 2px;
}
.adminRow {
display: flex;
align-items: center;
gap: 10px;
}
.adminOnlyTag {
border: 1px solid var(--bg-amber-500, #ffcc56);
border-radius: 99px;
padding: 1px 8px;
letter-spacing: 0.03em;
}
.footer {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
}
.footerSpacer {
flex: 1;
}

View File

@@ -0,0 +1,123 @@
import { Button } from '@signozhq/ui/button';
import { DrawerWrapper } from '@signozhq/ui/drawer';
import { Typography } from '@signozhq/ui/typography';
import { MetricreductionruletypesGettableReductionRuleDTO } from 'api/generated/services/sigNoz.schemas';
import { useVolumeControlConfig } from '../../hooks/useVolumeControlConfig';
import ImpactPanel from './ImpactPanel/ImpactPanel';
import LabelSelector from './LabelSelector/LabelSelector';
import ModeSelector from './ModeSelector/ModeSelector';
import RelatedAssetsWarning from './RelatedAssetsWarning/RelatedAssetsWarning';
import styles from './VolumeControlConfigDrawer.module.scss';
interface VolumeControlConfigDrawerProps {
metricName: string;
existingRule: MetricreductionruletypesGettableReductionRuleDTO | null;
open: boolean;
onClose: () => void;
}
function VolumeControlConfigDrawer({
metricName,
existingRule,
open,
onClose,
}: VolumeControlConfigDrawerProps): JSX.Element {
const {
mode,
setMode,
labels,
setLabels,
attributeKeys,
isLoadingAttributes,
preview,
isPreviewLoading,
save,
remove,
isSaving,
isRemoving,
hasExistingRule,
isSaveDisabled,
} = useVolumeControlConfig({ metricName, existingRule, open, onClose });
const footer = (
<div className={styles.footer}>
<Button
variant="outlined"
color="secondary"
onClick={onClose}
data-testid="volume-control-cancel"
>
Cancel
</Button>
<div className={styles.footerSpacer} />
{hasExistingRule && (
<Button
variant="ghost"
color="destructive"
onClick={remove}
loading={isRemoving}
data-testid="volume-control-remove"
>
Remove rule
</Button>
)}
<Button
variant="solid"
color="primary"
onClick={save}
disabled={isSaveDisabled}
loading={isSaving}
data-testid="volume-control-save"
>
Save rule
</Button>
</div>
);
return (
<DrawerWrapper
open={open}
onOpenChange={(next: boolean): void => {
if (!next) {
onClose();
}
}}
title={`Manage attributes · ${metricName}`}
direction="right"
showCloseButton
width="wide"
footer={footer}
showOverlay={false}
className="volume-control-config-drawer"
style={{ zIndex: 1100 }}
>
<div className={styles.body} data-testid="volume-control-config-drawer">
<div className={styles.adminRow}>
<Typography.Text
size="xs"
weight="semibold"
color="warning"
className={styles.adminOnlyTag}
>
Admin only
</Typography.Text>
</div>
<ModeSelector mode={mode} onChange={setMode} />
{mode !== 'all' && (
<LabelSelector
mode={mode}
options={attributeKeys}
value={labels}
onChange={setLabels}
loading={isLoadingAttributes}
/>
)}
<ImpactPanel mode={mode} preview={preview} isLoading={isPreviewLoading} />
<RelatedAssetsWarning affectedAssets={preview?.affectedAssets} />
</div>
</DrawerWrapper>
);
}
export default VolumeControlConfigDrawer;

View File

@@ -0,0 +1,12 @@
.emptyState {
display: flex;
flex-direction: column;
align-items: flex-start;
border: 1px dashed var(--bg-slate-300, #242834);
border-radius: 6px;
padding: 14px;
}
.setupButton {
margin-top: 12px;
}

View File

@@ -0,0 +1,36 @@
import { Button } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
import styles from './NoRuleEmptyState.module.scss';
interface NoRuleEmptyStateProps {
canManage: boolean;
onSetup: () => void;
}
function NoRuleEmptyState({
canManage,
onSetup,
}: NoRuleEmptyStateProps): JSX.Element {
return (
<div className={styles.emptyState} data-testid="volume-control-empty">
<Typography.Text size="small" color="muted">
No volume control rule. All series are retained. Aggregate away
high-cardinality attributes to reduce cost.
</Typography.Text>
{canManage && (
<Button
variant="solid"
color="primary"
className={styles.setupButton}
onClick={onSetup}
data-testid="volume-control-setup"
>
Set up volume control
</Button>
)}
</div>
);
}
export default NoRuleEmptyState;

View File

@@ -0,0 +1,10 @@
.banner {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 9px 12px;
border-radius: 6px;
background: var(--callout-primary-background);
border: 1px solid var(--callout-primary-border);
color: var(--callout-primary-icon);
}

View File

@@ -0,0 +1,18 @@
import { Info } from '@signozhq/icons';
import { Typography } from '@signozhq/ui/typography';
import styles from './PendingActivationBanner.module.scss';
function PendingActivationBanner(): JSX.Element {
return (
<div className={styles.banner} data-testid="volume-control-pending-banner">
<Info size={13} />
<Typography.Text size="sm" color="muted">
This metric&apos;s configuration was recently updated. Volume changes will
take effect within a few minutes.
</Typography.Text>
</div>
);
}
export default PendingActivationBanner;

View File

@@ -0,0 +1,48 @@
.card {
display: flex;
flex-direction: column;
gap: 8px;
border: 1px solid var(--bg-slate-400, #1d212d);
border-radius: 6px;
padding: 12px 14px;
background: var(--bg-ink-300, #16181d);
}
.cardRow {
display: flex;
align-items: center;
gap: 8px;
}
.statusIndicator {
width: 7px;
height: 7px;
border-radius: 50%;
flex: 0 0 auto;
}
.statusActive {
background: var(--bg-forest-500, #25e192);
}
.statusPending {
background: var(--bg-amber-500, #ffcc56);
}
.editButton {
margin-left: auto;
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.labelChip {
font-family: 'Geist Mono', monospace;
padding: 2px 8px;
border-radius: 4px;
background: var(--bg-ink-200, #23262e);
border: 1px solid var(--bg-slate-400, #1d212d);
}

View File

@@ -0,0 +1,60 @@
import { Button } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
import { MetricreductionruletypesGettableReductionRuleDTO } from 'api/generated/services/sigNoz.schemas';
import cx from 'classnames';
import { getLabelVerb, getMatchTypeLabel } from '../../../ruleUtils';
import styles from './RuleSummaryCard.module.scss';
interface RuleSummaryCardProps {
rule: MetricreductionruletypesGettableReductionRuleDTO;
canManage: boolean;
onEdit: () => void;
}
function RuleSummaryCard({
rule,
canManage,
onEdit,
}: RuleSummaryCardProps): JSX.Element {
return (
<div className={styles.card} data-testid="volume-control-active">
<div className={styles.cardRow}>
<span
className={cx(
styles.statusIndicator,
rule.active ? styles.statusActive : styles.statusPending,
)}
/>
<Typography.Text weight="semibold">
{rule.active
? 'Aggregation rule active'
: 'Aggregation rule pending activation'}
</Typography.Text>
{canManage && (
<Button
variant="ghost"
color="secondary"
className={styles.editButton}
onClick={onEdit}
data-testid="volume-control-edit"
>
Edit
</Button>
)}
</div>
<Typography.Text as="div" size="small" color="muted">
{getMatchTypeLabel(rule.matchType)}
</Typography.Text>
<div className={styles.chips}>
{(rule.labels ?? []).map((label) => (
<Typography.Text size="sm" className={styles.labelChip} key={label}>
{getLabelVerb(rule.matchType)} {label}
</Typography.Text>
))}
</div>
</div>
);
}
export default RuleSummaryCard;

View File

@@ -0,0 +1,16 @@
.section {
margin-top: 16px;
}
.header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
color: var(--bg-vanilla-400, #c0c1c3);
}
.title {
text-transform: uppercase;
letter-spacing: 0.05em;
}

View File

@@ -0,0 +1,84 @@
import { Gauge } from '@signozhq/icons';
import { Typography } from '@signozhq/ui/typography';
import { Skeleton } from 'antd';
import { useListMetricReductionRules } from 'api/generated/services/metrics';
import { useVolumeControlFeatureGate } from 'hooks/metricsExplorer/useVolumeControlFeatureGate';
import { useState } from 'react';
import NoRuleEmptyState from './NoRuleEmptyState/NoRuleEmptyState';
import PendingActivationBanner from './PendingActivationBanner/PendingActivationBanner';
import RuleSummaryCard from './RuleSummaryCard/RuleSummaryCard';
import VolumeControlConfigDrawer from '../VolumeControlConfigDrawer/VolumeControlConfigDrawer';
import styles from './VolumeControlSection.module.scss';
interface VolumeControlSectionProps {
metricName: string;
}
function VolumeControlSection({
metricName,
}: VolumeControlSectionProps): JSX.Element | null {
const { isVolumeControlEnabled, canManageVolumeControl } =
useVolumeControlFeatureGate();
const [isConfigOpen, setIsConfigOpen] = useState(false);
const { data, isLoading, error } = useListMetricReductionRules(
{ metricName },
{
query: {
enabled: isVolumeControlEnabled && !!metricName,
retry: false,
},
},
);
if (!isVolumeControlEnabled) {
return null;
}
const rule = data?.data.rules?.[0];
const hasRule = !!rule && !error;
const openConfig = (): void => setIsConfigOpen(true);
const closeConfig = (): void => setIsConfigOpen(false);
return (
<div className={styles.section} data-testid="volume-control-section">
<div className={styles.header}>
<Gauge size={14} />
<Typography.Text size="sm" weight="semibold" className={styles.title}>
Volume control
</Typography.Text>
</div>
{isLoading && <Skeleton active title={false} paragraph={{ rows: 2 }} />}
{!isLoading && hasRule && rule && !rule.active && (
<PendingActivationBanner />
)}
{!isLoading && hasRule && rule && (
<RuleSummaryCard
rule={rule}
canManage={canManageVolumeControl}
onEdit={openConfig}
/>
)}
{!isLoading && !hasRule && (
<NoRuleEmptyState canManage={canManageVolumeControl} onSetup={openConfig} />
)}
{canManageVolumeControl && isConfigOpen && (
<VolumeControlConfigDrawer
metricName={metricName}
existingRule={rule ?? null}
open={isConfigOpen}
onClose={closeConfig}
/>
)}
</div>
);
}
export default VolumeControlSection;

View File

@@ -0,0 +1,20 @@
.header {
display: flex;
flex-direction: column;
gap: 4px;
}
.titleRow {
display: flex;
align-items: center;
gap: 8px;
color: var(--bg-robin-400, #7190f9);
}
.title {
margin: 0 !important;
}
.subtitle {
max-width: 680px;
}

View File

@@ -0,0 +1,23 @@
import { Gauge } from '@signozhq/icons';
import { Typography } from '@signozhq/ui/typography';
import styles from './VolumeControlHeader.module.scss';
function VolumeControlHeader(): JSX.Element {
return (
<div className={styles.header}>
<div className={styles.titleRow}>
<Gauge size={18} />
<Typography.Title level={4} className={styles.title}>
Volume Control
</Typography.Title>
</div>
<Typography.Text size="small" color="muted" className={styles.subtitle}>
Aggregate away high-cardinality attributes to reduce stored metric volume
and cost.
</Typography.Text>
</div>
);
}
export default VolumeControlHeader;

View File

@@ -0,0 +1,34 @@
.stats {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.statCard {
display: flex;
flex: 1 1 0;
flex-direction: column;
gap: 6px;
min-width: 150px;
padding: 12px 16px;
border: 1px solid var(--bg-slate-400, #1d212d);
border-radius: 6px;
background: var(--bg-ink-400, #121317);
}
.statCardHighlighted {
border-color: var(--callout-success-border);
background: var(--callout-success-background);
}
.statCardLabel {
text-transform: uppercase;
letter-spacing: 0.05em;
}
.statCardValue {
display: flex;
align-items: baseline;
gap: 6px;
font-family: 'Geist Mono', monospace;
}

View File

@@ -0,0 +1,88 @@
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import { formatCompact, formatUsd } from '../../../configUtils';
import styles from './VolumeControlStats.module.scss';
interface VolumeControlStatsProps {
activeRules: number;
ingestedSeries: number;
retainedSeries: number;
estimatedMonthlySavingsUsd: number;
}
interface StatItem {
label: string;
value: string;
delta?: string;
unit?: string;
highlighted?: boolean;
valueGood?: boolean;
}
function VolumeControlStats({
activeRules,
ingestedSeries,
retainedSeries,
estimatedMonthlySavingsUsd,
}: VolumeControlStatsProps): JSX.Element {
const overallReduction =
ingestedSeries > 0
? Math.round((1 - retainedSeries / ingestedSeries) * 100)
: 0;
const items: StatItem[] = [
{ label: 'Active rules', value: String(activeRules) },
{ label: 'Ingested series', value: formatCompact(ingestedSeries) },
{
label: 'Retained series',
value: formatCompact(retainedSeries),
delta: overallReduction > 0 ? `${overallReduction}%` : undefined,
},
{
label: 'Est. monthly savings',
value: formatUsd(estimatedMonthlySavingsUsd),
unit: '/mo',
highlighted: true,
valueGood: true,
},
];
return (
<div className={styles.stats} data-testid="volume-control-stats">
{items.map((item) => (
<div
key={item.label}
className={cx(styles.statCard, {
[styles.statCardHighlighted]: item.highlighted,
})}
>
<Typography.Text size="sm" color="muted" className={styles.statCardLabel}>
{item.label}
</Typography.Text>
<Typography.Text
as="div"
size="large"
weight="semibold"
color={item.valueGood ? 'success' : undefined}
className={styles.statCardValue}
>
{item.value}
{item.delta && (
<Typography.Text size="small" weight="semibold" color="success">
{item.delta}
</Typography.Text>
)}
{item.unit && (
<Typography.Text size="small" weight="medium" color="muted">
{item.unit}
</Typography.Text>
)}
</Typography.Text>
</div>
))}
</div>
);
}
export default VolumeControlStats;

View File

@@ -0,0 +1,28 @@
.tab {
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px 0;
}
.metricNameCell {
font-family: 'Geist Mono', monospace;
}
.attributesCell {
font-family: 'Geist Mono', monospace;
}
.reductionCell {
font-family: 'Geist Mono', monospace;
}
.empty {
padding: 32px 16px;
text-align: center;
}
.unavailable {
padding: 48px 16px;
text-align: center;
}

View File

@@ -0,0 +1,319 @@
import { Button } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
import { Table } from 'antd';
import type { TableColumnsType, TableProps } from 'antd';
import {
useGetMetricReductionRuleStats,
useListMetricReductionRules,
} from 'api/generated/services/metrics';
import {
ListMetricReductionRulesParams,
MetricreductionruletypesGettableReductionRuleDTO,
MetricreductionruletypesOrderDTO,
MetricreductionruletypesReductionRuleOrderByDTO,
} from 'api/generated/services/sigNoz.schemas';
import dayjs from 'dayjs';
import { useVolumeControlFeatureGate } from 'hooks/metricsExplorer/useVolumeControlFeatureGate';
import useDebounce from 'hooks/useDebounce';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { formatCompact } from '../../configUtils';
import { getLabelVerb, getMatchTypeLabel } from '../../ruleUtils';
import VolumeControlBadge from '../VolumeControlBadge';
import VolumeControlChart from '../VolumeControlChart/VolumeControlChart';
import VolumeControlConfigDrawer from '../VolumeControlConfigDrawer/VolumeControlConfigDrawer';
import VolumeControlHeader from './VolumeControlHeader/VolumeControlHeader';
import VolumeControlStats from './VolumeControlStats/VolumeControlStats';
import styles from './VolumeControlTab.module.scss';
import VolumeControlToolbar from './VolumeControlToolbar/VolumeControlToolbar';
const OrderBy = MetricreductionruletypesReductionRuleOrderByDTO;
const SortOrder = MetricreductionruletypesOrderDTO;
const DEFAULT_PAGE_SIZE = 10;
type VolumeControlTableParams = Required<
Omit<ListMetricReductionRulesParams, 'metricName'>
>;
const DEFAULT_PARAMS: VolumeControlTableParams = {
orderBy: OrderBy.reduction,
order: SortOrder.desc,
search: '',
offset: 0,
limit: DEFAULT_PAGE_SIZE,
};
function VolumeControlTab(): JSX.Element {
const { isVolumeControlEnabled, canManageVolumeControl } =
useVolumeControlFeatureGate();
const [selectedRule, setSelectedRule] =
useState<MetricreductionruletypesGettableReductionRuleDTO | null>(null);
const [params, setParams] = useState<VolumeControlTableParams>(DEFAULT_PARAMS);
const [searchInput, setSearchInput] = useState('');
const debouncedSearch = useDebounce(searchInput, 400);
useEffect(() => {
setParams((prev) =>
prev.search === debouncedSearch
? prev
: { ...prev, search: debouncedSearch, offset: 0 },
);
}, [debouncedSearch]);
const { data, isLoading } = useListMetricReductionRules(params, {
query: { enabled: isVolumeControlEnabled },
});
const { data: statsData } = useGetMetricReductionRuleStats({
query: { enabled: isVolumeControlEnabled },
});
const stats = statsData?.data;
const rules = data?.data.rules ?? [];
const total = data?.data.total ?? 0;
const sortOrderFor = useCallback(
(
key: MetricreductionruletypesReductionRuleOrderByDTO,
): 'ascend' | 'descend' | undefined => {
if (params.orderBy !== key) {
return undefined;
}
return params.order === SortOrder.desc ? 'descend' : 'ascend';
},
[params],
);
const columns: TableColumnsType<MetricreductionruletypesGettableReductionRuleDTO> =
useMemo(
() => [
{
title: 'METRIC',
dataIndex: 'metricName',
key: OrderBy.metric,
sorter: true,
sortOrder: sortOrderFor(OrderBy.metric),
render: (metricName: string): JSX.Element => (
<Typography.Text size="small" className={styles.metricNameCell}>
{metricName}
</Typography.Text>
),
},
{
title: 'STATUS',
key: 'status',
width: 130,
render: (
_value: unknown,
rule: MetricreductionruletypesGettableReductionRuleDTO,
): JSX.Element => <VolumeControlBadge rule={rule} />,
},
{
title: 'MODE',
key: 'mode',
width: 160,
render: (
_value: unknown,
rule: MetricreductionruletypesGettableReductionRuleDTO,
): JSX.Element => (
<Typography.Text size="small">
{getMatchTypeLabel(rule.matchType)}
</Typography.Text>
),
},
{
title: 'ATTRIBUTES',
key: 'attributes',
render: (
_value: unknown,
rule: MetricreductionruletypesGettableReductionRuleDTO,
): JSX.Element => (
<Typography.Text
size="small"
color="muted"
className={styles.attributesCell}
>
{getLabelVerb(rule.matchType)} {(rule.labels ?? []).join(', ') || '—'}
</Typography.Text>
),
},
{
title: 'INGESTED',
key: OrderBy.ingested_volume,
width: 130,
sorter: true,
sortOrder: sortOrderFor(OrderBy.ingested_volume),
render: (
_value: unknown,
rule: MetricreductionruletypesGettableReductionRuleDTO,
): JSX.Element => (
<Typography.Text size="small" color="muted">
{formatCompact(rule.ingestedSeries)}
</Typography.Text>
),
},
{
title: 'RETAINED',
key: OrderBy.reduced_volume,
width: 130,
sorter: true,
sortOrder: sortOrderFor(OrderBy.reduced_volume),
render: (
_value: unknown,
rule: MetricreductionruletypesGettableReductionRuleDTO,
): JSX.Element => (
<Typography.Text size="small">
{formatCompact(rule.retainedSeries)}
</Typography.Text>
),
},
{
title: 'CHANGE',
key: OrderBy.reduction,
width: 110,
sorter: true,
sortOrder: sortOrderFor(OrderBy.reduction),
render: (
_value: unknown,
rule: MetricreductionruletypesGettableReductionRuleDTO,
): JSX.Element => {
if (rule.reductionPercent <= 0) {
return (
<Typography.Text size="small" color="muted">
</Typography.Text>
);
}
return (
<Typography.Text
size="small"
weight="semibold"
color="success"
className={styles.reductionCell}
>
{Math.round(rule.reductionPercent)}%
</Typography.Text>
);
},
},
{
title: 'LAST CONFIGURED',
key: OrderBy.last_updated,
width: 240,
sorter: true,
sortOrder: sortOrderFor(OrderBy.last_updated),
render: (
_value: unknown,
rule: MetricreductionruletypesGettableReductionRuleDTO,
): JSX.Element => (
<Typography.Text size="small" color="muted">
{dayjs(rule.updatedAt).format('MMM D, YYYY · h:mm A')}
{rule.updatedBy ? ` · ${rule.updatedBy}` : ''}
</Typography.Text>
),
},
...(canManageVolumeControl
? ([
{
title: '',
key: 'action',
width: 110,
render: (
_value: unknown,
rule: MetricreductionruletypesGettableReductionRuleDTO,
): JSX.Element => (
<Button
variant="ghost"
color="secondary"
onClick={(): void => setSelectedRule(rule)}
data-testid={`volume-control-manage-${rule.metricName}`}
>
Manage
</Button>
),
},
] as TableColumnsType<MetricreductionruletypesGettableReductionRuleDTO>)
: []),
],
[canManageVolumeControl, sortOrderFor],
);
const handleTableChange: TableProps<MetricreductionruletypesGettableReductionRuleDTO>['onChange'] =
(pagination, _filters, sorter): void => {
const active = Array.isArray(sorter) ? sorter[0] : sorter;
const pageSize = pagination.pageSize ?? DEFAULT_PAGE_SIZE;
const current = pagination.current ?? 1;
setParams((prev) => ({
...prev,
orderBy: active?.order
? (active.columnKey as MetricreductionruletypesReductionRuleOrderByDTO)
: DEFAULT_PARAMS.orderBy,
order: active?.order === 'descend' ? SortOrder.desc : SortOrder.asc,
limit: pageSize,
offset: (current - 1) * pageSize,
}));
};
if (!isVolumeControlEnabled) {
return (
<div className={styles.unavailable} data-testid="volume-control-unavailable">
<Typography.Text color="muted">
Volume control is available on enterprise and cloud plans.
</Typography.Text>
</div>
);
}
return (
<div className={styles.tab} data-testid="volume-control-tab">
<VolumeControlHeader />
<VolumeControlStats
activeRules={total}
ingestedSeries={stats?.ingestedSeries ?? 0}
retainedSeries={stats?.retainedSeries ?? 0}
estimatedMonthlySavingsUsd={stats?.estimatedMonthlySavingsUsd ?? 0}
/>
<VolumeControlChart enabled={isVolumeControlEnabled} />
<VolumeControlToolbar value={searchInput} onChange={setSearchInput} />
<Table<MetricreductionruletypesGettableReductionRuleDTO>
rowKey="metricName"
loading={isLoading}
dataSource={rules}
columns={columns}
onChange={handleTableChange}
pagination={{
current: Math.floor(params.offset / params.limit) + 1,
pageSize: params.limit,
total,
showSizeChanger: false,
}}
locale={{
emptyText: (
<div className={styles.empty} data-testid="volume-control-tab-empty">
<Typography.Text color="muted">
No volume control rules yet. Open a metric and set one up to start
reducing its series volume.
</Typography.Text>
</div>
),
}}
/>
{canManageVolumeControl && selectedRule && (
<VolumeControlConfigDrawer
metricName={selectedRule.metricName}
existingRule={selectedRule}
open={!!selectedRule}
onClose={(): void => setSelectedRule(null)}
/>
)}
</div>
);
}
export default VolumeControlTab;

View File

@@ -0,0 +1,8 @@
.toolbar {
display: flex;
justify-content: flex-end;
}
.search {
max-width: 320px;
}

View File

@@ -0,0 +1,28 @@
import { Input } from 'antd';
import styles from './VolumeControlToolbar.module.scss';
interface VolumeControlToolbarProps {
value: string;
onChange: (value: string) => void;
}
function VolumeControlToolbar({
value,
onChange,
}: VolumeControlToolbarProps): JSX.Element {
return (
<div className={styles.toolbar}>
<Input
className={styles.search}
placeholder="Search metrics"
allowClear
value={value}
onChange={(e): void => onChange(e.target.value)}
data-testid="volume-control-search"
/>
</div>
);
}
export default VolumeControlToolbar;

View File

@@ -0,0 +1,49 @@
import {
MetricreductionruletypesMatchTypeDTO,
MetricreductionruletypesGettableReductionRuleDTO,
} from 'api/generated/services/sigNoz.schemas';
import { RuleMode } from './types';
export function modeFromRule(
rule: MetricreductionruletypesGettableReductionRuleDTO | null | undefined,
): { mode: RuleMode; labels: string[] } {
if (!rule) {
return { mode: 'all', labels: [] };
}
return {
mode:
rule.matchType === MetricreductionruletypesMatchTypeDTO.keep
? 'include'
: 'exclude',
labels: rule.labels ?? [],
};
}
export function matchTypeForMode(
mode: RuleMode,
): MetricreductionruletypesMatchTypeDTO {
return mode === 'include'
? MetricreductionruletypesMatchTypeDTO.keep
: MetricreductionruletypesMatchTypeDTO.drop;
}
export function formatCompact(value: number): string {
if (value >= 1e9) {
return `${(value / 1e9).toFixed(1)}B`;
}
if (value >= 1e6) {
return `${(value / 1e6).toFixed(1)}M`;
}
if (value >= 1e3) {
return `${(value / 1e3).toFixed(1)}K`;
}
return `${value}`;
}
export function formatUsd(value: number): string {
if (value >= 1e3) {
return `$${(value / 1e3).toFixed(1)}K`;
}
return `$${value.toFixed(2)}`;
}

View File

@@ -0,0 +1,211 @@
import {
invalidateListMetricReductionRules,
invalidateListMetrics,
useCreateMetricReductionRule,
useDeleteMetricReductionRuleByID,
useGetMetricAttributes,
usePreviewMetricReductionRule,
useUpdateMetricReductionRuleByID,
} from 'api/generated/services/metrics';
import {
MetricreductionruletypesGettableReductionRuleDTO,
MetricreductionruletypesGettableReductionRulePreviewDTO,
} from 'api/generated/services/sigNoz.schemas';
import { useNotifications } from 'hooks/useNotifications';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQueryClient } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { matchTypeForMode, modeFromRule } from '../configUtils';
import { RuleMode } from '../types';
interface UseVolumeControlConfigParams {
metricName: string;
existingRule: MetricreductionruletypesGettableReductionRuleDTO | null;
open: boolean;
onClose: () => void;
}
export interface UseVolumeControlConfigResult {
mode: RuleMode;
setMode: (mode: RuleMode) => void;
labels: string[];
setLabels: (labels: string[]) => void;
attributeKeys: string[];
isLoadingAttributes: boolean;
preview?: MetricreductionruletypesGettableReductionRulePreviewDTO;
isPreviewLoading: boolean;
save: () => void;
remove: () => void;
isSaving: boolean;
isRemoving: boolean;
hasExistingRule: boolean;
isSaveDisabled: boolean;
}
const PREVIEW_DEBOUNCE_MS = 400;
const SAVE_ERROR_MESSAGE = 'Failed to save volume control rule';
const REMOVE_ERROR_MESSAGE = 'Failed to remove volume control rule';
export function useVolumeControlConfig({
metricName,
existingRule,
open,
onClose,
}: UseVolumeControlConfigParams): UseVolumeControlConfigResult {
const { notifications } = useNotifications();
const queryClient = useQueryClient();
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const initial = useMemo(() => modeFromRule(existingRule), [existingRule]);
const [mode, setMode] = useState<RuleMode>(initial.mode);
const [labels, setLabels] = useState<string[]>(initial.labels);
const existingRuleId = existingRule?.id;
const attributesQuery = useGetMetricAttributes(
{
metricName,
start: minTime ? Math.floor(minTime / 1000000) : undefined,
end: maxTime ? Math.floor(maxTime / 1000000) : undefined,
},
{ query: { enabled: open && !!metricName } },
);
const attributeKeys = useMemo(
() => (attributesQuery.data?.data.attributes ?? []).map((attr) => attr.key),
[attributesQuery.data],
);
const previewMutation = usePreviewMetricReductionRule();
const { mutate: previewMutate, reset: previewReset } = previewMutation;
const [isPreviewPending, setIsPreviewPending] = useState(false);
useEffect(() => {
if (!open || mode === 'all' || labels.length === 0) {
previewReset();
setIsPreviewPending(false);
return undefined;
}
setIsPreviewPending(true);
const timer = setTimeout(() => {
previewMutate(
{ data: { metricName, matchType: matchTypeForMode(mode), labels } },
{ onSettled: () => setIsPreviewPending(false) },
);
}, PREVIEW_DEBOUNCE_MS);
return (): void => clearTimeout(timer);
}, [open, mode, labels, metricName, previewMutate, previewReset]);
const createMutation = useCreateMetricReductionRule();
const updateMutation = useUpdateMetricReductionRuleByID();
const deleteMutation = useDeleteMetricReductionRuleByID();
const invalidate = useCallback((): void => {
void invalidateListMetricReductionRules(queryClient);
void invalidateListMetrics(queryClient);
}, [queryClient]);
const removeRule = useCallback((): void => {
if (!existingRuleId) {
onClose();
return;
}
deleteMutation.mutate(
{ pathParams: { id: existingRuleId } },
{
onSuccess: () => {
notifications.success({ message: 'Volume control rule removed' });
invalidate();
onClose();
},
onError: (error) =>
notifications.error({
message: error.response?.data?.error?.message ?? REMOVE_ERROR_MESSAGE,
}),
},
);
}, [deleteMutation, existingRuleId, notifications, invalidate, onClose]);
const save = useCallback((): void => {
if (mode === 'all') {
if (!existingRuleId) {
onClose();
return;
}
removeRule();
return;
}
const onSuccess = (): void => {
notifications.success({ message: 'Volume control rule saved' });
invalidate();
onClose();
};
if (existingRuleId) {
updateMutation.mutate(
{
pathParams: { id: existingRuleId },
data: { matchType: matchTypeForMode(mode), labels },
},
{
onSuccess,
onError: (error) =>
notifications.error({
message: error.response?.data?.error?.message ?? SAVE_ERROR_MESSAGE,
}),
},
);
return;
}
createMutation.mutate(
{
data: { metricName, matchType: matchTypeForMode(mode), labels },
},
{
onSuccess,
onError: (error) =>
notifications.error({
message: error.response?.data?.error?.message ?? SAVE_ERROR_MESSAGE,
}),
},
);
}, [
mode,
labels,
metricName,
existingRuleId,
createMutation,
updateMutation,
removeRule,
notifications,
invalidate,
onClose,
]);
return {
mode,
setMode,
labels,
setLabels,
attributeKeys,
isLoadingAttributes: attributesQuery.isLoading,
preview: previewMutation.data?.data,
isPreviewLoading: isPreviewPending,
save,
remove: removeRule,
isSaving:
createMutation.isLoading ||
updateMutation.isLoading ||
deleteMutation.isLoading,
isRemoving: deleteMutation.isLoading,
hasExistingRule: !!existingRuleId,
isSaveDisabled: mode !== 'all' && labels.length === 0,
};
}

View File

@@ -0,0 +1,19 @@
import { MetricreductionruletypesMatchTypeDTO } from 'api/generated/services/sigNoz.schemas';
export function isKeepMode(
matchType: MetricreductionruletypesMatchTypeDTO,
): boolean {
return matchType === MetricreductionruletypesMatchTypeDTO.keep;
}
export function getMatchTypeLabel(
matchType: MetricreductionruletypesMatchTypeDTO,
): string {
return isKeepMode(matchType) ? 'Include attributes' : 'Exclude attributes';
}
export function getLabelVerb(
matchType: MetricreductionruletypesMatchTypeDTO,
): string {
return isKeepMode(matchType) ? 'include' : 'exclude';
}

View File

@@ -0,0 +1 @@
export type RuleMode = 'all' | 'include' | 'exclude';

View File

@@ -33,15 +33,7 @@ export default function NoLogs({
} else if (dataSource === DataSource.METRICS) {
logEvent('Metrics Explorer: Navigate to onboarding', {});
}
let link;
if (dataSource === DataSource.TRACES) {
link = ROUTES.GET_STARTED_APPLICATION_MONITORING;
} else if (dataSource === DataSource.METRICS) {
link = ROUTES.GET_STARTED_WITH_CLOUD;
} else {
link = ROUTES.GET_STARTED_LOGS_MANAGEMENT;
}
history.push(link);
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else if (dataSource === 'traces') {
openInNewTab(DOCLINKS.TRACES_EXPLORER_EMPTY_STATE);
} else if (dataSource === DataSource.METRICS) {

View File

@@ -1,106 +0,0 @@
### Step 1: Install OpenTelemetry Dependencies
Dependencies related to OpenTelemetry exporter and SDK have to be installed first.
Run the below commands after navigating to the application source folder:
```bash
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.Runtime
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.AutoInstrumentation
```
&nbsp;
### Step 2: Adding OpenTelemetry as a service and configuring exporter options
In your `Program.cs` file, add OpenTelemetry as a service. Here, we are configuring these variables:
`serviceName` - It is the name of your service.
`otlpOptions.Endpoint` - It is the endpoint for your OTel Collector agent.
&nbsp;
Heres a sample `Program.cs` file with the configured variables:
```bash
using System.Diagnostics;
using OpenTelemetry.Exporter;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
// Configure OpenTelemetry with tracing and auto-start.
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource =>
resource.AddService(serviceName: "{{MYAPP}}"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddOtlpExporter(otlpOptions =>
{
//sigNoz Cloud Endpoint
otlpOptions.Endpoint = new Uri("https://ingest.{{REGION}}.signoz.cloud:443");
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
//SigNoz Cloud account Ingestion key
string headerKey = "signoz-ingestion-key";
string headerValue = "{{SIGNOZ_INGESTION_KEY}}";
string formattedHeader = $"{headerKey}={headerValue}";
otlpOptions.Headers = formattedHeader;
}));
var app = builder.Build();
//The index route ("/") is set up to write out the OpenTelemetry trace information on the response:
app.MapGet("/", () => $"Hello World! OpenTelemetry Trace: {Activity.Current?.Id}");
app.Run();
```
&nbsp;
The OpenTelemetry.Exporter.Options get or set the target to which the exporter is going to send traces. Here, were configuring it to send traces to the OTel Collector agent. The target must be a valid Uri with the scheme (http or https) and host and may contain a port and a path.
This is done by configuring an OpenTelemetry [TracerProvider](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/trace/customizing-the-sdk#readme) using extension methods and setting it to auto-start when the host is started.
### Step 3: Dockerize your application
Since the environment variables like SIGNOZ_INGESTION_KEY, Ingestion Endpoint and Service name are set in the `program.cs` file, you don't need to add any additional steps in your Dockerfile.
An **example** of a Dockerfile could look like this:
```bash
# Use the Microsoft official .NET SDK image to build the application
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env
WORKDIR /app
# Copy the CSPROJ file and restore any dependencies (via NUGET)
COPY *.csproj ./
RUN dotnet restore
# Copy the rest of the project files and build the application
COPY . ./
RUN dotnet publish -c Release -o out
# Generate the runtime image
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build-env /app/out .
# Expose port 5145 for the application
EXPOSE 5145
# Set the ASPNETCORE_URLS environment variable to listen on port 5145
ENV ASPNETCORE_URLS=http://+:5145
ENTRYPOINT ["dotnet", "YOUR-APPLICATION.dll"]
```

View File

@@ -1,21 +0,0 @@
Once you update your Dockerfile, you can build and run it using the commands below.
&nbsp;
### Step 1: Build your dockerfile
Build your docker image
```bash
docker build -t <your-image-name> .
```
- `<your-image-name>` is the name of your Docker Image
&nbsp;
### Step 2: Run your docker image
```bash
docker run <your-image-name>
```

View File

@@ -1,12 +0,0 @@
## Setup OpenTelemetry Binary as an agent
&nbsp;
As a first step, you should install the OTel collector Binary according to the instructions provided on [this link](https://signoz.io/docs/tutorial/opentelemetry-binary-usage-in-virtual-machine/).
&nbsp;
Once you are done setting up the OTel collector binary, you can follow the next steps.
&nbsp;

View File

@@ -1,101 +0,0 @@
After setting up the Otel collector agent, follow the steps below to instrument your .NET Application
&nbsp;
&nbsp;
### Step 1: Install OpenTelemetry Dependencies
Install the following dependencies in your application.
```bash
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.Runtime
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.AutoInstrumentation
```
&nbsp;
### Step 2: Adding OpenTelemetry as a service and configuring exporter options
In your `Program.cs` file, add OpenTelemetry as a service. Here, we are configuring these variables:
`serviceName` - It is the name of your service.
`otlpOptions.Endpoint` - It is the endpoint for your OTel Collector agent.
&nbsp;
Heres a sample `Program.cs` file with the configured variables:
```bash
using System.Diagnostics;
using OpenTelemetry.Exporter;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
// Configure OpenTelemetry with tracing and auto-start.
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource =>
resource.AddService(serviceName: "{{MYAPP}}"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddOtlpExporter(otlpOptions =>
{
otlpOptions.Endpoint = new Uri("http://localhost:4317");
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
}));
var app = builder.Build();
//The index route ("/") is set up to write out the OpenTelemetry trace information on the response:
app.MapGet("/", () => $"Hello World! OpenTelemetry Trace: {Activity.Current?.Id}");
app.Run();
```
&nbsp;
The OpenTelemetry.Exporter.Options get or set the target to which the exporter is going to send traces. Here, were configuring it to send traces to the OTel Collector agent. The target must be a valid Uri with the scheme (http or https) and host and may contain a port and a path.
This is done by configuring an OpenTelemetry [TracerProvider](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/trace/customizing-the-sdk#readme) using extension methods and setting it to auto-start when the host is started.
&nbsp;
### Step 3: Dockerize your application
Since the crucial environment variables like SIGNOZ_INGESTION_KEY, Ingestion Endpoint and Service name are set in the `program.cs` file, you don't need to add any additional steps in your Dockerfile.
An **example** of a Dockerfile could look like this:
```bash
# Use the Microsoft official .NET SDK image to build the application
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env
WORKDIR /app
# Copy the CSPROJ file and restore any dependencies (via NUGET)
COPY *.csproj ./
RUN dotnet restore
# Copy the rest of the project files and build the application
COPY . ./
RUN dotnet publish -c Release -o out
# Generate the runtime image
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build-env /app/out .
# Expose port 5145 for the application
EXPOSE 5145
# Set the ASPNETCORE_URLS environment variable to listen on port 5145
ENV ASPNETCORE_URLS=http://+:5145
ENTRYPOINT ["dotnet", "YOUR-APPLICATION.dll"]
```

View File

@@ -1,21 +0,0 @@
Once you update your Dockerfile, you can build and run it using the commands below.
&nbsp;
### Step 1: Build your dockerfile
Build your docker image
```bash
docker build -t <your-image-name> .
```
- `<your-image-name>` is the name of your Docker Image
&nbsp;
### Step 2: Run your docker image
```bash
docker run <your-image-name>
```

View File

@@ -1,43 +0,0 @@
### Install otel-collector in your Kubernetes infra
Add the SigNoz Helm Chart repository
```bash
helm repo add signoz https://charts.signoz.io
```
&nbsp;
If the chart is already present, update the chart to the latest using:
```bash
helm repo update
```
&nbsp;
For generic Kubernetes clusters, you can create *override-values.yaml* with the following configuration:
```yaml
global:
cloud: others
clusterName: <CLUSTER_NAME>
deploymentEnvironment: <DEPLOYMENT_ENVIRONMENT>
otelCollectorEndpoint: ingest.{{REGION}}.signoz.cloud:443
otelInsecure: false
signozApiKey: {{SIGNOZ_INGESTION_KEY}}
presets:
otlpExporter:
enabled: true
loggingExporter:
enabled: false
```
- Replace `<CLUSTER_NAME>` with the name of the Kubernetes cluster or a unique identifier of the cluster.
- Replace `<DEPLOYMENT_ENVIRONMENT>` with the deployment environment of your application. Example: **"staging"**, **"production"**, etc.
&nbsp;
To install the k8s-infra chart with the above configuration, run the following command:
```bash
helm install my-release signoz/k8s-infra -f override-values.yaml
```

View File

@@ -1,65 +0,0 @@
After setting up the Otel collector agent, follow the steps below to instrument your .NET Application
&nbsp;
&nbsp;
### Step 1: Install OpenTelemetry Dependencies
Install the following dependencies in your application.
```bash
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.Runtime
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.AutoInstrumentation
```
&nbsp;
### Step 2: Adding OpenTelemetry as a service and configuring exporter options
In your `Program.cs` file, add OpenTelemetry as a service. Here, we are configuring these variables:
`serviceName` - It is the name of your service.
`otlpOptions.Endpoint` - It is the endpoint for your OTel Collector agent.
&nbsp;
Heres a sample `Program.cs` file with the configured variables:
```bash
using System.Diagnostics;
using OpenTelemetry.Exporter;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
// Configure OpenTelemetry with tracing and auto-start.
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource =>
resource.AddService(serviceName: "{{MYAPP}}"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddOtlpExporter(otlpOptions =>
{
otlpOptions.Endpoint = new Uri("http://localhost:4317");
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
}));
var app = builder.Build();
//The index route ("/") is set up to write out the OpenTelemetry trace information on the response:
app.MapGet("/", () => $"Hello World! OpenTelemetry Trace: {Activity.Current?.Id}");
app.Run();
```
The OpenTelemetry.Exporter.Options get or set the target to which the exporter is going to send traces. Here, were configuring it to send traces to the OTel Collector agent. The target must be a valid Uri with the scheme (http or https) and host and may contain a port and a path.
This is done by configuring an OpenTelemetry [TracerProvider](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/trace/customizing-the-sdk#readme) using extension methods and setting it to auto-start when the host is started.

View File

@@ -1,10 +0,0 @@
&nbsp;
To run your .NET application, use the below command :
```bash
dotnet build
dotnet run
```
Once you run your .NET application, interact with your application to generate some load and see your application in the SigNoz UI.

View File

@@ -1,71 +0,0 @@
### Step 1: Install OpenTelemetry Dependencies
Dependencies related to OpenTelemetry exporter and SDK have to be installed first.
Run the below commands after navigating to the application source folder:
```bash
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.Runtime
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.AutoInstrumentation
```
&nbsp;
### Step 2: Adding OpenTelemetry as a service and configuring exporter options
In your `Program.cs` file, add OpenTelemetry as a service. Here, we are configuring these variables:
`serviceName` - It is the name of your service.
`otlpOptions.Endpoint` - It is the endpoint for your OTel Collector agent.
&nbsp;
Heres a sample `Program.cs` file with the configured variables:
```bash
using System.Diagnostics;
using OpenTelemetry.Exporter;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
// Configure OpenTelemetry with tracing and auto-start.
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource =>
resource.AddService(serviceName: "{{MYAPP}}"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddOtlpExporter(otlpOptions =>
{
//sigNoz Cloud Endpoint
otlpOptions.Endpoint = new Uri("https://ingest.{{REGION}}.signoz.cloud:443");
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
//SigNoz Cloud account Ingestion key
string headerKey = "signoz-ingestion-key";
string headerValue = "{{SIGNOZ_INGESTION_KEY}}";
string formattedHeader = $"{headerKey}={headerValue}";
otlpOptions.Headers = formattedHeader;
}));
var app = builder.Build();
//The index route ("/") is set up to write out the OpenTelemetry trace information on the response:
app.MapGet("/", () => $"Hello World! OpenTelemetry Trace: {Activity.Current?.Id}");
app.Run();
```
&nbsp;
The OpenTelemetry.Exporter.Options get or set the target to which the exporter is going to send traces. Here, were configuring it to send traces to the OTel Collector agent. The target must be a valid Uri with the scheme (http or https) and host and may contain a port and a path.
This is done by configuring an OpenTelemetry [TracerProvider](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/trace/customizing-the-sdk#readme) using extension methods and setting it to auto-start when the host is started.

View File

@@ -1,10 +0,0 @@
&nbsp;
To run your .NET application, use the below command :
```bash
dotnet build
dotnet run
```
Once you run your .NET application, interact with your application to generate some load and see your application in the SigNoz UI.

View File

@@ -1,98 +0,0 @@
## Setup OpenTelemetry Binary as an agent
&nbsp;
### Step 1: Download otel-collector tar.gz
```bash
wget https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v{{OTEL_VERSION}}/otelcol-contrib_{{OTEL_VERSION}}_linux_amd64.tar.gz
```
&nbsp;
### Step 2: Extract otel-collector tar.gz to the `otelcol-contrib` folder
```bash
mkdir otelcol-contrib && tar xvzf otelcol-contrib_{{OTEL_VERSION}}_linux_amd64.tar.gz -C otelcol-contrib
```
&nbsp;
### Step 3: Create `config.yaml` in `otelcol-contrib` folder with the below content in it
```bash
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
hostmetrics:
collection_interval: 60s
scrapers:
cpu: {}
disk: {}
load: {}
filesystem: {}
memory: {}
network: {}
paging: {}
process:
mute_process_name_error: true
mute_process_exe_error: true
mute_process_io_error: true
processes: {}
prometheus:
config:
global:
scrape_interval: 60s
scrape_configs:
- job_name: otel-collector-binary
static_configs:
- targets:
# - localhost:8888
processors:
batch:
send_batch_size: 1000
timeout: 10s
# Ref: https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/processor/resourcedetectionprocessor/README.md
resourcedetection:
detectors: [env, system] # Before system detector, include ec2 for AWS, gcp for GCP and azure for Azure.
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
timeout: 2s
system:
hostname_sources: [os] # alternatively, use [dns,os] for setting FQDN as host.name and os as fallback
extensions:
health_check: {}
zpages: {}
exporters:
otlp:
endpoint: "ingest.{{REGION}}.signoz.cloud:443"
tls:
insecure: false
headers:
"signoz-ingestion-key": "{{SIGNOZ_INGESTION_KEY}}"
logging:
verbosity: normal
service:
telemetry:
metrics:
address: 0.0.0.0:8888
extensions: [health_check, zpages]
pipelines:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
metrics/internal:
receivers: [prometheus, hostmetrics]
processors: [resourcedetection, batch]
exporters: [otlp]
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
logs:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
```

View File

@@ -1,67 +0,0 @@
After setting up the Otel collector agent, follow the steps below to instrument your .NET Application
&nbsp;
&nbsp;
### Step 1: Install OpenTelemetry Dependencies
Install the following dependencies in your application.
```bash
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.Runtime
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.AutoInstrumentation
```
&nbsp;
### Step 2: Adding OpenTelemetry as a service and configuring exporter options
In your `Program.cs` file, add OpenTelemetry as a service. Here, we are configuring these variables:
`serviceName` - It is the name of your service.
`otlpOptions.Endpoint` - It is the endpoint for your OTel Collector agent.
&nbsp;
Heres a sample `Program.cs` file with the configured variables:
```bash
using System.Diagnostics;
using OpenTelemetry.Exporter;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
// Configure OpenTelemetry with tracing and auto-start.
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource =>
resource.AddService(serviceName: "{{MYAPP}}"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddOtlpExporter(otlpOptions =>
{
otlpOptions.Endpoint = new Uri("http://localhost:4317");
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
}));
var app = builder.Build();
//The index route ("/") is set up to write out the OpenTelemetry trace information on the response:
app.MapGet("/", () => $"Hello World! OpenTelemetry Trace: {Activity.Current?.Id}");
app.Run();
```
&nbsp;
The OpenTelemetry.Exporter.Options get or set the target to which the exporter is going to send traces. Here, were configuring it to send traces to the OTel Collector agent. The target must be a valid Uri with the scheme (http or https) and host and may contain a port and a path.
This is done by configuring an OpenTelemetry [TracerProvider](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/trace/customizing-the-sdk#readme) using extension methods and setting it to auto-start when the host is started.

View File

@@ -1,18 +0,0 @@
&nbsp;
Once you are done intrumenting your .NET application, you can run it using the below commands
&nbsp;
### Step 1: Run OTel Collector
Run this command inside the `otelcol-contrib` directory that you created in the install Otel Collector step
```bash
./otelcol-contrib --config ./config.yaml
```
&nbsp;
### Step 2: Run your .NET application
```bash
dotnet build
dotnet run
```

View File

@@ -1,70 +0,0 @@
### Step 1: Install OpenTelemetry Dependencies
Dependencies related to OpenTelemetry exporter and SDK have to be installed first.
Run the below commands after navigating to the application source folder:
```bash
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.Runtime
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.AutoInstrumentation
```
&nbsp;
### Step 2: Adding OpenTelemetry as a service and configuring exporter options
In your `Program.cs` file, add OpenTelemetry as a service. Here, we are configuring these variables:
`serviceName` - It is the name of your service.
`otlpOptions.Endpoint` - It is the endpoint for your OTel Collector agent.
&nbsp;
Heres a sample `Program.cs` file with the configured variables:
```bash
using System.Diagnostics;
using OpenTelemetry.Exporter;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
// Configure OpenTelemetry with tracing and auto-start.
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource =>
resource.AddService(serviceName: "{{MYAPP}}"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddOtlpExporter(otlpOptions =>
{
//sigNoz Cloud Endpoint
otlpOptions.Endpoint = new Uri("https://ingest.{{REGION}}.signoz.cloud:443");
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
//SigNoz Cloud account Ingestion key
string headerKey = "signoz-ingestion-key";
string headerValue = "{{SIGNOZ_INGESTION_KEY}}";
string formattedHeader = $"{headerKey}={headerValue}";
otlpOptions.Headers = formattedHeader;
}));
var app = builder.Build();
//The index route ("/") is set up to write out the OpenTelemetry trace information on the response:
app.MapGet("/", () => $"Hello World! OpenTelemetry Trace: {Activity.Current?.Id}");
app.Run();
```
&nbsp;
The OpenTelemetry.Exporter.Options get or set the target to which the exporter is going to send traces. Here, were configuring it to send traces to the OTel Collector agent. The target must be a valid Uri with the scheme (http or https) and host and may contain a port and a path.
This is done by configuring an OpenTelemetry [TracerProvider](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/trace/customizing-the-sdk#readme) using extension methods and setting it to auto-start when the host is started.

View File

@@ -1,10 +0,0 @@
&nbsp;
To run your .NET application, use the below command :
```bash
dotnet build
dotnet run
```
Once you run your .NET application, interact with your application to generate some load and see your application in the SigNoz UI.

View File

@@ -1,99 +0,0 @@
## Setup OpenTelemetry Binary as an agent
&nbsp;
### Step 1: Download otel-collector tar.gz
```bash
wget https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v{{OTEL_VERSION}}/otelcol-contrib_{{OTEL_VERSION}}_linux_arm64.tar.gz
```
&nbsp;
### Step 2: Extract otel-collector tar.gz to the `otelcol-contrib` folder
```bash
mkdir otelcol-contrib && tar xvzf otelcol-contrib_{{OTEL_VERSION}}_linux_arm64.tar.gz -C otelcol-contrib
```
&nbsp;
### Step 3: Create `config.yaml` in `otelcol-contrib` folder with the below content in it
```bash
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
hostmetrics:
collection_interval: 60s
scrapers:
cpu: {}
disk: {}
load: {}
filesystem: {}
memory: {}
network: {}
paging: {}
process:
mute_process_name_error: true
mute_process_exe_error: true
mute_process_io_error: true
processes: {}
prometheus:
config:
global:
scrape_interval: 60s
scrape_configs:
- job_name: otel-collector-binary
static_configs:
- targets:
# - localhost:8888
processors:
batch:
send_batch_size: 1000
timeout: 10s
# Ref: https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/processor/resourcedetectionprocessor/README.md
resourcedetection:
detectors: [env, system] # Before system detector, include ec2 for AWS, gcp for GCP and azure for Azure.
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
timeout: 2s
system:
hostname_sources: [os] # alternatively, use [dns,os] for setting FQDN as host.name and os as fallback
extensions:
health_check: {}
zpages: {}
exporters:
otlp:
endpoint: "ingest.{{REGION}}.signoz.cloud:443"
tls:
insecure: false
headers:
"signoz-ingestion-key": "{{SIGNOZ_INGESTION_KEY}}"
logging:
verbosity: normal
service:
telemetry:
metrics:
address: 0.0.0.0:8888
extensions: [health_check, zpages]
pipelines:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
metrics/internal:
receivers: [prometheus, hostmetrics]
processors: [resourcedetection, batch]
exporters: [otlp]
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
logs:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
```

View File

@@ -1,68 +0,0 @@
After setting up the Otel collector agent, follow the steps below to instrument your .NET Application
&nbsp;
&nbsp;
### Step 1: Install OpenTelemetry Dependencies
Install the following dependencies in your application.
```bash
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.Runtime
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.AutoInstrumentation
```
&nbsp;
### Step 2: Adding OpenTelemetry as a service and configuring exporter options
In your `Program.cs` file, add OpenTelemetry as a service. Here, we are configuring these variables:
`serviceName` - It is the name of your service.
`otlpOptions.Endpoint` - It is the endpoint for your OTel Collector agent.
&nbsp;
Heres a sample `Program.cs` file with the configured variables:
```bash
using System.Diagnostics;
using OpenTelemetry.Exporter;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
// Configure OpenTelemetry with tracing and auto-start.
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource =>
resource.AddService(serviceName: "{{MYAPP}}"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddOtlpExporter(otlpOptions =>
{
otlpOptions.Endpoint = new Uri("http://localhost:4317");
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
}));
var app = builder.Build();
//The index route ("/") is set up to write out the OpenTelemetry trace information on the response:
app.MapGet("/", () => $"Hello World! OpenTelemetry Trace: {Activity.Current?.Id}");
app.Run();
```
&nbsp;
The OpenTelemetry.Exporter.Options get or set the target to which the exporter is going to send traces. Here, were configuring it to send traces to the OTel Collector agent. The target must be a valid Uri with the scheme (http or https) and host and may contain a port and a path.
This is done by configuring an OpenTelemetry [TracerProvider](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/trace/customizing-the-sdk#readme) using extension methods and setting it to auto-start when the host is started.

View File

@@ -1,18 +0,0 @@
&nbsp;
Once you are done intrumenting your .NET application, you can run it using the below commands
&nbsp;
### Step 1: Run OTel Collector
Run this command inside the `otelcol-contrib` directory that you created in the install Otel Collector step
```bash
./otelcol-contrib --config ./config.yaml
```
&nbsp;
### Step 2: Run your .NET application
```bash
dotnet build
dotnet run
```

View File

@@ -1,70 +0,0 @@
### Step 1: Install OpenTelemetry Dependencies
Dependencies related to OpenTelemetry exporter and SDK have to be installed first.
Run the below commands after navigating to the application source folder:
```bash
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.Runtime
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.AutoInstrumentation
```
&nbsp;
### Step 2: Adding OpenTelemetry as a service and configuring exporter options
In your `Program.cs` file, add OpenTelemetry as a service. Here, we are configuring these variables:
`serviceName` - It is the name of your service.
`otlpOptions.Endpoint` - It is the endpoint for your OTel Collector agent.
&nbsp;
Heres a sample `Program.cs` file with the configured variables:
```bash
using System.Diagnostics;
using OpenTelemetry.Exporter;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
// Configure OpenTelemetry with tracing and auto-start.
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource =>
resource.AddService(serviceName: "{{MYAPP}}"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddOtlpExporter(otlpOptions =>
{
//sigNoz Cloud Endpoint
otlpOptions.Endpoint = new Uri("https://ingest.{{REGION}}.signoz.cloud:443");
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
//SigNoz Cloud account Ingestion key
string headerKey = "signoz-ingestion-key";
string headerValue = "{{SIGNOZ_INGESTION_KEY}}";
string formattedHeader = $"{headerKey}={headerValue}";
otlpOptions.Headers = formattedHeader;
}));
var app = builder.Build();
//The index route ("/") is set up to write out the OpenTelemetry trace information on the response:
app.MapGet("/", () => $"Hello World! OpenTelemetry Trace: {Activity.Current?.Id}");
app.Run();
```
&nbsp;
The OpenTelemetry.Exporter.Options get or set the target to which the exporter is going to send traces. Here, were configuring it to send traces to the OTel Collector agent. The target must be a valid Uri with the scheme (http or https) and host and may contain a port and a path.
This is done by configuring an OpenTelemetry [TracerProvider](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/trace/customizing-the-sdk#readme) using extension methods and setting it to auto-start when the host is started.

View File

@@ -1,10 +0,0 @@
&nbsp;
To run your .NET application, use the below command :
```bash
dotnet build
dotnet run
```
Once you run your .NET application, interact with your application to generate some load and see your application in the SigNoz UI.

View File

@@ -1,97 +0,0 @@
## Setup OpenTelemetry Binary as an agent
&nbsp;
### Step 1: Download otel-collector tar.gz
```bash
wget https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v{{OTEL_VERSION}}/otelcol-contrib_{{OTEL_VERSION}}_darwin_amd64.tar.gz
```
&nbsp;
### Step 2: Extract otel-collector tar.gz to the `otelcol-contrib` folder
```bash
mkdir otelcol-contrib && tar xvzf otelcol-contrib_{{OTEL_VERSION}}_darwin_amd64.tar.gz -C otelcol-contrib
```
&nbsp;
### Step 3: Create `config.yaml` in folder `otelcol-contrib` with the below content in it
```bash
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
hostmetrics:
collection_interval: 60s
scrapers:
cpu: {}
disk: {}
load: {}
filesystem: {}
memory: {}
network: {}
paging: {}
process:
mute_process_name_error: true
mute_process_exe_error: true
mute_process_io_error: true
processes: {}
prometheus:
config:
global:
scrape_interval: 60s
scrape_configs:
- job_name: otel-collector-binary
static_configs:
- targets:
# - localhost:8888
processors:
batch:
send_batch_size: 1000
timeout: 10s
# Ref: https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/processor/resourcedetectionprocessor/README.md
resourcedetection:
detectors: [env, system] # Before system detector, include ec2 for AWS, gcp for GCP and azure for Azure.
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
timeout: 2s
system:
hostname_sources: [os] # alternatively, use [dns,os] for setting FQDN as host.name and os as fallback
extensions:
health_check: {}
zpages: {}
exporters:
otlp:
endpoint: "ingest.{{REGION}}.signoz.cloud:443"
tls:
insecure: false
headers:
"signoz-ingestion-key": "{{SIGNOZ_INGESTION_KEY}}"
logging:
verbosity: normal
service:
telemetry:
metrics:
address: 0.0.0.0:8888
extensions: [health_check, zpages]
pipelines:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
metrics/internal:
receivers: [prometheus, hostmetrics]
processors: [resourcedetection, batch]
exporters: [otlp]
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
logs:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
```

View File

@@ -1,67 +0,0 @@
After setting up the Otel collector agent, follow the steps below to instrument your .NET Application
&nbsp;
&nbsp;
### Step 1: Install OpenTelemetry Dependencies
Install the following dependencies in your application.
```bash
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.Runtime
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.AutoInstrumentation
```
&nbsp;
### Step 2: Adding OpenTelemetry as a service and configuring exporter options
In your `Program.cs` file, add OpenTelemetry as a service. Here, we are configuring these variables:
`serviceName` - It is the name of your service.
`otlpOptions.Endpoint` - It is the endpoint for your OTel Collector agent.
&nbsp;
Heres a sample `Program.cs` file with the configured variables:
```bash
using System.Diagnostics;
using OpenTelemetry.Exporter;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
// Configure OpenTelemetry with tracing and auto-start.
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource =>
resource.AddService(serviceName: "{{MYAPP}}"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddOtlpExporter(otlpOptions =>
{
otlpOptions.Endpoint = new Uri("http://localhost:4317");
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
}));
var app = builder.Build();
//The index route ("/") is set up to write out the OpenTelemetry trace information on the response:
app.MapGet("/", () => $"Hello World! OpenTelemetry Trace: {Activity.Current?.Id}");
app.Run();
```
&nbsp;
The OpenTelemetry.Exporter.Options get or set the target to which the exporter is going to send traces. Here, were configuring it to send traces to the OTel Collector agent. The target must be a valid Uri with the scheme (http or https) and host and may contain a port and a path.
This is done by configuring an OpenTelemetry [TracerProvider](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/trace/customizing-the-sdk#readme) using extension methods and setting it to auto-start when the host is started.

View File

@@ -1,18 +0,0 @@
&nbsp;
Once you are done intrumenting your .NET application, you can run it using the below commands
&nbsp;
### Step 1: Run OTel Collector
Run this command inside the `otelcol-contrib` directory that you created in the install Otel Collector step
```bash
./otelcol-contrib --config ./config.yaml
```
&nbsp;
### Step 2: Run your .NET application
```bash
dotnet build
dotnet run
```

View File

@@ -1,70 +0,0 @@
### Step 1: Install OpenTelemetry Dependencies
Dependencies related to OpenTelemetry exporter and SDK have to be installed first.
Run the below commands after navigating to the application source folder:
```bash
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.Runtime
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.AutoInstrumentation
```
&nbsp;
### Step 2: Adding OpenTelemetry as a service and configuring exporter options
In your `Program.cs` file, add OpenTelemetry as a service. Here, we are configuring these variables:
`serviceName` - It is the name of your service.
`otlpOptions.Endpoint` - It is the endpoint for your OTel Collector agent.
&nbsp;
Heres a sample `Program.cs` file with the configured variables:
```bash
using System.Diagnostics;
using OpenTelemetry.Exporter;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
// Configure OpenTelemetry with tracing and auto-start.
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource =>
resource.AddService(serviceName: "{{MYAPP}}"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddOtlpExporter(otlpOptions =>
{
//sigNoz Cloud Endpoint
otlpOptions.Endpoint = new Uri("https://ingest.{{REGION}}.signoz.cloud:443");
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
//SigNoz Cloud account Ingestion key
string headerKey = "signoz-ingestion-key";
string headerValue = "{{SIGNOZ_INGESTION_KEY}}";
string formattedHeader = $"{headerKey}={headerValue}";
otlpOptions.Headers = formattedHeader;
}));
var app = builder.Build();
//The index route ("/") is set up to write out the OpenTelemetry trace information on the response:
app.MapGet("/", () => $"Hello World! OpenTelemetry Trace: {Activity.Current?.Id}");
app.Run();
```
&nbsp;
The OpenTelemetry.Exporter.Options get or set the target to which the exporter is going to send traces. Here, were configuring it to send traces to the OTel Collector agent. The target must be a valid Uri with the scheme (http or https) and host and may contain a port and a path.
This is done by configuring an OpenTelemetry [TracerProvider](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/trace/customizing-the-sdk#readme) using extension methods and setting it to auto-start when the host is started.

View File

@@ -1,10 +0,0 @@
&nbsp;
To run your .NET application, use the below command :
```bash
dotnet build
dotnet run
```
Once you run your .NET application, interact with your application to generate some load and see your application in the SigNoz UI.

View File

@@ -1,98 +0,0 @@
## Setup OpenTelemetry Binary as an agent
&nbsp;
### Step 1: Download otel-collector tar.gz
```bash
wget https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v{{OTEL_VERSION}}/otelcol-contrib_{{OTEL_VERSION}}_darwin_arm64.tar.gz
```
&nbsp;
### Step 2: Extract otel-collector tar.gz to the `otelcol-contrib` folder
```bash
mkdir otelcol-contrib && tar xvzf otelcol-contrib_{{OTEL_VERSION}}_darwin_arm64.tar.gz -C otelcol-contrib
```
&nbsp;
### Step 3: Create `config.yaml` in folder `otelcol-contrib` with the below content in it
```bash
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
hostmetrics:
collection_interval: 60s
scrapers:
cpu: {}
disk: {}
load: {}
filesystem: {}
memory: {}
network: {}
paging: {}
process:
mute_process_name_error: true
mute_process_exe_error: true
mute_process_io_error: true
processes: {}
prometheus:
config:
global:
scrape_interval: 60s
scrape_configs:
- job_name: otel-collector-binary
static_configs:
- targets:
# - localhost:8888
processors:
batch:
send_batch_size: 1000
timeout: 10s
# Ref: https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/processor/resourcedetectionprocessor/README.md
resourcedetection:
detectors: [env, system] # Before system detector, include ec2 for AWS, gcp for GCP and azure for Azure.
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
timeout: 2s
system:
hostname_sources: [os] # alternatively, use [dns,os] for setting FQDN as host.name and os as fallback
extensions:
health_check: {}
zpages: {}
exporters:
otlp:
endpoint: "ingest.{{REGION}}.signoz.cloud:443"
tls:
insecure: false
headers:
"signoz-ingestion-key": "{{SIGNOZ_INGESTION_KEY}}"
logging:
verbosity: normal
service:
telemetry:
metrics:
address: 0.0.0.0:8888
extensions: [health_check, zpages]
pipelines:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
metrics/internal:
receivers: [prometheus, hostmetrics]
processors: [resourcedetection, batch]
exporters: [otlp]
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
logs:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
```

View File

@@ -1,68 +0,0 @@
After setting up the Otel collector agent, follow the steps below to instrument your .NET Application
&nbsp;
&nbsp;
### Step 1: Install OpenTelemetry Dependencies
Install the following dependencies in your application.
```bash
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.Runtime
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.AutoInstrumentation
```
&nbsp;
### Step 2: Adding OpenTelemetry as a service and configuring exporter options
In your `Program.cs` file, add OpenTelemetry as a service. Here, we are configuring these variables:
`serviceName` - It is the name of your service.
`otlpOptions.Endpoint` - It is the endpoint for your OTel Collector agent.
&nbsp;
Heres a sample `Program.cs` file with the configured variables:
```bash
using System.Diagnostics;
using OpenTelemetry.Exporter;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
// Configure OpenTelemetry with tracing and auto-start.
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource =>
resource.AddService(serviceName: "{{MYAPP}}"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddOtlpExporter(otlpOptions =>
{
otlpOptions.Endpoint = new Uri("http://localhost:4317");
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
}));
var app = builder.Build();
//The index route ("/") is set up to write out the OpenTelemetry trace information on the response:
app.MapGet("/", () => $"Hello World! OpenTelemetry Trace: {Activity.Current?.Id}");
app.Run();
```
&nbsp;
The OpenTelemetry.Exporter.Options get or set the target to which the exporter is going to send traces. Here, were configuring it to send traces to the OTel Collector agent. The target must be a valid Uri with the scheme (http or https) and host and may contain a port and a path.
This is done by configuring an OpenTelemetry [TracerProvider](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/trace/customizing-the-sdk#readme) using extension methods and setting it to auto-start when the host is started.

View File

@@ -1,18 +0,0 @@
&nbsp;
Once you are done intrumenting your .NET application, you can run it using the below commands
&nbsp;
### Step 1: Run OTel Collector
Run this command inside the `otelcol-contrib` directory that you created in the install Otel Collector step
```bash
./otelcol-contrib --config ./config.yaml
```
&nbsp;
### Step 2: Run your .NET application
```bash
dotnet build
dotnet run
```

View File

@@ -1,67 +0,0 @@
**Step 1: Installing the OpenTelemetry dependency packages:**
```bash
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.Runtime
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.AutoInstrumentation
```
**Step 2: Adding OpenTelemetry as a service and configuring exporter options in `Program.cs`:**
In your `Program.cs` file, add OpenTelemetry as a service.
Heres a sample `Program.cs` file with the configured variables.
```bash
using System.Diagnostics;
using OpenTelemetry.Exporter;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
// Configure OpenTelemetry with tracing and auto-start.
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource =>
resource.AddService(serviceName: "{{MYAPP}}"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddOtlpExporter(otlpOptions =>
{
//SigNoz Cloud Endpoint
otlpOptions.Endpoint = new Uri("https://ingest.{{REGION}}.signoz.cloud:443");
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
//SigNoz Cloud account Ingestion key
string headerKey = "signoz-ingestion-key";
string headerValue = "{{SIGNOZ_INGESTION_KEY}}";
string formattedHeader = $"{headerKey}={headerValue}";
otlpOptions.Headers = formattedHeader;
}));
var app = builder.Build();
// The index route ("/") is set up to write out the OpenTelemetry trace information on the response:
app.MapGet("/", () => $"Hello World! OpenTelemetry Trace: {Activity.Current?.Id}");
app.Run();
```
**Step 3. Running the .NET application:**
```bash
dotnet build
dotnet run
```
**Step 4: Generating some load data and checking your application in SigNoz UI**
Once your application is running, generate some traffic by interacting with it.
In the SigNoz account, open the `Services` tab. Hit the `Refresh` button on the top right corner, and your application should appear in the list of `Applications`. Ensure that you're checking data for the `time range filter` applied in the top right corner. You might have to wait for a few seconds before the data appears on SigNoz UI.

View File

@@ -1,10 +0,0 @@
&nbsp;
To run your .NET application, use the below command :
```bash
dotnet build
dotnet run
```
Once you run your .NET application, interact with your application to generate some load and see your application in the SigNoz UI.

View File

@@ -1,12 +0,0 @@
## Setup OpenTelemetry Binary as an agent
&nbsp;
As a first step, you should install the OTel collector Binary according to the instructions provided on [this link](https://signoz.io/docs/tutorial/opentelemetry-binary-usage-in-virtual-machine/).
&nbsp;
Once you are done setting up the OTel collector binary, you can follow the next steps.
&nbsp;

View File

@@ -1,63 +0,0 @@
After setting up the Otel collector agent, follow the steps below to instrument your .NET Application
&nbsp;
&nbsp;
### Step 1: Install OpenTelemetry Dependencies
Install the following dependencies in your application.
```bash
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.Runtime
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.AutoInstrumentation
```
&nbsp;
### Step 2: Adding OpenTelemetry as a service and configuring exporter options
In your `Program.cs` file, add OpenTelemetry as a service. Here, we are configuring these variables:
&nbsp;
Heres a sample `Program.cs` file with the configured variables:
```bash
using System.Diagnostics;
using OpenTelemetry.Exporter;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
// Configure OpenTelemetry with tracing and auto-start.
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource =>
resource.AddService(serviceName: "{{MYAPP}}"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddOtlpExporter(otlpOptions =>
{
otlpOptions.Endpoint = new Uri("http://localhost:4317");
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
}));
var app = builder.Build();
//The index route ("/") is set up to write out the OpenTelemetry trace information on the response:
app.MapGet("/", () => $"Hello World! OpenTelemetry Trace: {Activity.Current?.Id}");
app.Run();
```
&nbsp;
The OpenTelemetry.Exporter.Options get or set the target to which the exporter is going to send traces. Here, were configuring it to send traces to the OTel Collector agent. The target must be a valid Uri with the scheme (http or https) and host and may contain a port and a path.
This is done by configuring an OpenTelemetry [TracerProvider](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/trace/customizing-the-sdk#readme) using extension methods and setting it to auto-start when the host is started.

View File

@@ -1,18 +0,0 @@
&nbsp;
Once you are done intrumenting your .NET application, you can run it using the below commands
&nbsp;
### Step 1: Run OTel Collector
Run this command inside the `otelcol-contrib` directory that you created in the install Otel Collector step
```bash
./otelcol-contrib --config ./config.yaml
```
&nbsp;
### Step 2: Run your .NET application
```bash
dotnet build
dotnet run
```

View File

@@ -1,67 +0,0 @@
&nbsp;
Follow the steps below to instrument your Elixir (Phoenix + Ecto) Application
### Step 1: Add dependencies
Install dependencies related to OpenTelemetry by adding them to `mix.exs` file
```bash
{:opentelemetry_exporter, "~> 1.6"},
{:opentelemetry_api, "~> 1.2"},
{:opentelemetry, "~> 1.3"},
{:opentelemetry_semantic_conventions, "~> 0.2"},
{:opentelemetry_cowboy, "~> 0.2.1"},
{:opentelemetry_phoenix, "~> 1.1"},
{:opentelemetry_ecto, "~> 1.1"}
```
&nbsp;
In your application start, usually the `application.ex` file, setup the telemetry handlers
```bash
:opentelemetry_cowboy.setup()
OpentelemetryPhoenix.setup(adapter: :cowboy2)
OpentelemetryEcto.setup([:{{MYAPP}}, :repo])
```
&nbsp;
As an example, this is how you can setup the handlers in your application.ex file for an application called demo :
```bash
# application.ex
@impl true
def start(_type, _args) do
:opentelemetry_cowboy.setup()
OpentelemetryPhoenix.setup(adapter: :cowboy2)
OpentelemetryEcto.setup([:demo, :repo])
end
```
&nbsp;
### Step 2: Configure Application
You need to configure your application to send telemetry data by adding the following config to your `runtime.exs` file:
```bash
config :opentelemetry, :resource, service: %{name: "{{MYAPP}}"}
config :opentelemetry, :processors,
otel_batch_processor: %{
exporter: {
:opentelemetry_exporter,
%{
endpoints: ["https://ingest.{{REGION}}.signoz.cloud:443"],
headers: [
{"signoz-ingestion-key", {{SIGNOZ_ACCESS_TOKEN}} }
]
}
}
}
```
&nbsp;
### Step 3: Dockerize your application
Since the environment variables like SIGNOZ_INGESTION_KEY, Ingestion Endpoint and Service name are set in the above steps, you don't need to add any additional steps in your Dockerfile.

View File

@@ -1,25 +0,0 @@
Once you update your Dockerfile, you can build and run it using the commands below.
&nbsp;
### Step 1: Build your dockerfile
Build your docker image
```bash
docker build -t <your-image-name> .
```
- `<your-image-name>` is the name of your Docker Image
&nbsp;
### Step 2: Run your docker image
```bash
docker run <your-image-name>
```
&nbsp;
To see some examples for instrumented applications, you can checkout [this link](https://signoz.io/docs/instrumentation/elixir/#sample-examples)

View File

@@ -1,12 +0,0 @@
## Setup OpenTelemetry Binary as an agent
&nbsp;
As a first step, you should install the OTel collector Binary according to the instructions provided on [this link](https://signoz.io/docs/tutorial/opentelemetry-binary-usage-in-virtual-machine/).
&nbsp;
Once you are done setting up the OTel collector binary, you can follow the next steps.
&nbsp;

View File

@@ -1,61 +0,0 @@
&nbsp;
After setting up the Otel collector agent, follow the steps below to instrument your Elixir (Phoenix + Ecto) Application
### Step 1: Add dependencies
Install dependencies related to OpenTelemetry by adding them to `mix.exs` file
```bash
{:opentelemetry_exporter, "~> 1.6"},
{:opentelemetry_api, "~> 1.2"},
{:opentelemetry, "~> 1.3"},
{:opentelemetry_semantic_conventions, "~> 0.2"},
{:opentelemetry_cowboy, "~> 0.2.1"},
{:opentelemetry_phoenix, "~> 1.1"},
{:opentelemetry_ecto, "~> 1.1"}
```
&nbsp;
In your application start, usually the `application.ex` file, setup the telemetry handlers
```bash
:opentelemetry_cowboy.setup()
OpentelemetryPhoenix.setup(adapter: :cowboy2)
OpentelemetryEcto.setup([:{{MYAPP}}, :repo])
```
&nbsp;
As an example, this is how you can setup the handlers in your application.ex file for an application called demo :
```bash
# application.ex
@impl true
def start(_type, _args) do
:opentelemetry_cowboy.setup()
OpentelemetryPhoenix.setup(adapter: :cowboy2)
OpentelemetryEcto.setup([:demo, :repo])
end
```
&nbsp;
### Step 2: Configure Application
You need to configure your application to send telemetry data by adding the following config to your `runtime.exs` file:
```bash
config :opentelemetry, :resource, service: %{name: "{{MYAPP}}"}
config :opentelemetry, :processors,
otel_batch_processor: %{
exporter:
{:opentelemetry_exporter,
%{endpoints: ["http://localhost:4318"]}
}
}
```
&nbsp;
### Step 3: Dockerize your application
Since the environment variables like SIGNOZ_INGESTION_KEY, Ingestion Endpoint and Service name are set in the above steps, you don't need to add any additional steps in your Dockerfile.

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