mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-06 02:20:31 +01:00
Compare commits
48 Commits
refactor/t
...
feat/billi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28e19b85f5 | ||
|
|
28d37122a4 | ||
|
|
860d14f5fa | ||
|
|
01678e422b | ||
|
|
8a7028a75d | ||
|
|
9fcdb613cb | ||
|
|
3616022049 | ||
|
|
5d270b716b | ||
|
|
19d9d26051 | ||
|
|
522148362b | ||
|
|
781158ecab | ||
|
|
0603bd6b27 | ||
|
|
37c57e6c05 | ||
|
|
12a2e63a31 | ||
|
|
453bcc06c4 | ||
|
|
fac5fe6b9e | ||
|
|
0ad412b844 | ||
|
|
d1957b5eac | ||
|
|
dba9cfd455 | ||
|
|
ed2011a7bb | ||
|
|
68385478c7 | ||
|
|
eb661b7ac7 | ||
|
|
afd6868423 | ||
|
|
8ddf0a13c1 | ||
|
|
16f0d2aa38 | ||
|
|
3af912c586 | ||
|
|
ad7715802b | ||
|
|
b579bdbd7b | ||
|
|
aa64cf7bbf | ||
|
|
2d33b1a743 | ||
|
|
4fbf7de8e1 | ||
|
|
7528b19fd4 | ||
|
|
42e4196aad | ||
|
|
22cdb03702 | ||
|
|
6eca3dc06e | ||
|
|
0631189417 | ||
|
|
ec552b94cc | ||
|
|
ee8d99f1d0 | ||
|
|
bf77e26a86 | ||
|
|
9cd3cf23d7 | ||
|
|
4a44802ebc | ||
|
|
f2aed0d834 | ||
|
|
527d8c0459 | ||
|
|
8fdc91260e | ||
|
|
218c4524b1 | ||
|
|
02dec846eb | ||
|
|
99dadb7247 | ||
|
|
44b41c40de |
@@ -18,16 +18,19 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/cache"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/gateway"
|
||||
"github.com/SigNoz/signoz/pkg/gateway/noopgateway"
|
||||
"github.com/SigNoz/signoz/pkg/global"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
|
||||
"github.com/SigNoz/signoz/pkg/meterreporter"
|
||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
|
||||
"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/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/retention"
|
||||
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
@@ -109,6 +112,9 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
func(_ licensing.Licensing) factory.NamedMap[factory.ProviderFactory[auditor.Auditor, auditor.Config]] {
|
||||
return signoz.NewAuditorProviderFactories()
|
||||
},
|
||||
func(_ context.Context, _ flagger.Flagger, _ licensing.Licensing, _ telemetrystore.TelemetryStore, _ retention.Getter, _ organization.Getter, _ zeus.Zeus) (factory.NamedMap[factory.ProviderFactory[meterreporter.Reporter, meterreporter.Config]], string) {
|
||||
return signoz.NewMeterReporterProviderFactories(), "noop"
|
||||
},
|
||||
func(ps factory.ProviderSettings, q querier.Querier, a analytics.Analytics) querier.Handler {
|
||||
return querier.NewHandler(ps, q, a)
|
||||
},
|
||||
|
||||
@@ -17,6 +17,14 @@ import (
|
||||
"github.com/SigNoz/signoz/ee/gateway/httpgateway"
|
||||
enterpriselicensing "github.com/SigNoz/signoz/ee/licensing"
|
||||
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
|
||||
"github.com/SigNoz/signoz/ee/metercollector/baseplatformfeemetercollector"
|
||||
"github.com/SigNoz/signoz/ee/metercollector/datapointcountmetercollector"
|
||||
"github.com/SigNoz/signoz/ee/metercollector/datapointsizemetercollector"
|
||||
"github.com/SigNoz/signoz/ee/metercollector/logcountmetercollector"
|
||||
"github.com/SigNoz/signoz/ee/metercollector/logsizemetercollector"
|
||||
"github.com/SigNoz/signoz/ee/metercollector/spancountmetercollector"
|
||||
"github.com/SigNoz/signoz/ee/metercollector/spansizemetercollector"
|
||||
"github.com/SigNoz/signoz/ee/meterreporter/httpmeterreporter"
|
||||
"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"
|
||||
@@ -35,14 +43,18 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/cache"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
pkgflagger "github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/gateway"
|
||||
"github.com/SigNoz/signoz/pkg/global"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/metercollector"
|
||||
"github.com/SigNoz/signoz/pkg/meterreporter"
|
||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
|
||||
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/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/retention"
|
||||
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
@@ -57,7 +69,10 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/zeustypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/SigNoz/signoz/pkg/version"
|
||||
"github.com/SigNoz/signoz/pkg/zeus"
|
||||
)
|
||||
@@ -157,6 +172,19 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
}
|
||||
return factories
|
||||
},
|
||||
func(ctx context.Context, flagger pkgflagger.Flagger, licensing licensing.Licensing, telemetryStore telemetrystore.TelemetryStore, retentionGetter retention.Getter, orgGetter organization.Getter, zeus zeus.Zeus) (factory.NamedMap[factory.ProviderFactory[meterreporter.Reporter, meterreporter.Config]], string) {
|
||||
factories := signoz.NewMeterReporterProviderFactories()
|
||||
if err := factories.Add(httpmeterreporter.NewFactory(newMeterCollectors(licensing, telemetryStore, retentionGetter), licensing, telemetryStore, orgGetter, zeus)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
evalCtx := featuretypes.NewFlaggerEvaluationContext(valuer.UUID{})
|
||||
if flagger.BooleanOrEmpty(ctx, pkgflagger.FeatureUseMeterReporter, evalCtx) {
|
||||
return factories, "http"
|
||||
}
|
||||
|
||||
return factories, "noop"
|
||||
},
|
||||
func(ps factory.ProviderSettings, q querier.Querier, a analytics.Analytics) querier.Handler {
|
||||
communityHandler := querier.NewHandler(ps, q, a)
|
||||
return eequerier.NewHandler(ps, q, communityHandler)
|
||||
@@ -216,3 +244,15 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newMeterCollectors(licensing licensing.Licensing, telemetryStore telemetrystore.TelemetryStore, retentionGetter retention.Getter) map[zeustypes.MeterName]metercollector.MeterCollector {
|
||||
return map[zeustypes.MeterName]metercollector.MeterCollector{
|
||||
baseplatformfeemetercollector.MeterName: baseplatformfeemetercollector.New(licensing),
|
||||
logcountmetercollector.MeterName: logcountmetercollector.New(telemetryStore, retentionGetter),
|
||||
logsizemetercollector.MeterName: logsizemetercollector.New(telemetryStore, retentionGetter),
|
||||
datapointcountmetercollector.MeterName: datapointcountmetercollector.New(telemetryStore, retentionGetter),
|
||||
datapointsizemetercollector.MeterName: datapointsizemetercollector.New(telemetryStore, retentionGetter),
|
||||
spancountmetercollector.MeterName: spancountmetercollector.New(telemetryStore, retentionGetter),
|
||||
spansizemetercollector.MeterName: spansizemetercollector.New(telemetryStore, retentionGetter),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -429,3 +429,10 @@ authz:
|
||||
openfga:
|
||||
# maximum tuples allowed per openfga write operation.
|
||||
max_tuples_per_write: 300
|
||||
|
||||
##################### Meter Reporter #####################
|
||||
meterreporter:
|
||||
# The interval between collection ticks. Minimum 5m.
|
||||
interval: 6h
|
||||
# The per-tick timeout that bounds collect-and-ship work. Minimum 3m and must be less than interval.
|
||||
timeout: 5m
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module base
|
||||
|
||||
type organization
|
||||
type organization
|
||||
relations
|
||||
define read: [user, serviceaccount, role#assignee]
|
||||
define update: [user, serviceaccount, role#assignee]
|
||||
@@ -46,4 +46,4 @@ type metaresource
|
||||
|
||||
type telemetryresource
|
||||
relations
|
||||
define read: [user, serviceaccount, role#assignee]
|
||||
define read: [user, serviceaccount, role#assignee]
|
||||
|
||||
53
ee/metercollector/baseplatformfeemetercollector/provider.go
Normal file
53
ee/metercollector/baseplatformfeemetercollector/provider.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// Package baseplatformfeemetercollector collects the license-derived base platform fee meter.
|
||||
package baseplatformfeemetercollector
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/metercollector"
|
||||
"github.com/SigNoz/signoz/pkg/types/zeustypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// MeterName is the typed registry key for this collector.
|
||||
var (
|
||||
MeterName = zeustypes.MustNewMeterName("signoz.meter.base.platform.fee")
|
||||
meterUnit = zeustypes.MeterUnitCount
|
||||
meterAggregation = zeustypes.MeterAggregationMax
|
||||
)
|
||||
|
||||
var _ metercollector.MeterCollector = (*Provider)(nil)
|
||||
|
||||
// Provider collects base platform fee meters.
|
||||
type Provider struct {
|
||||
licensing licensing.Licensing
|
||||
}
|
||||
|
||||
func New(licensing licensing.Licensing) *Provider {
|
||||
return &Provider{licensing: licensing}
|
||||
}
|
||||
|
||||
func (p *Provider) Name() zeustypes.MeterName { return MeterName }
|
||||
func (p *Provider) Unit() zeustypes.MeterUnit { return meterUnit }
|
||||
func (p *Provider) Aggregation() zeustypes.MeterAggregation {
|
||||
return meterAggregation
|
||||
}
|
||||
|
||||
// Collect emits value 1 when the org has an active license.
|
||||
func (p *Provider) Collect(ctx context.Context, orgID valuer.UUID, window zeustypes.MeterWindow) ([]zeustypes.Meter, error) {
|
||||
license, err := p.licensing.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "fetch active license for base platform fee meter")
|
||||
}
|
||||
if license == nil || license.Key == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return []zeustypes.Meter{
|
||||
zeustypes.NewMeter(MeterName, 1, meterUnit, meterAggregation, window, map[string]string{
|
||||
zeustypes.MeterDimensionOrganizationID: orgID.StringValue(),
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package baseplatformfeemetercollector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/types/licensetypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/zeustypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCollectEmitsBasePlatformFeeMeterForValidLicense(t *testing.T) {
|
||||
orgID := valuer.GenerateUUID()
|
||||
window := completedWindow()
|
||||
provider := New(&fakeLicensing{
|
||||
license: &licensetypes.License{Key: "license-key"},
|
||||
})
|
||||
|
||||
readings, err := provider.Collect(context.Background(), orgID, window)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []zeustypes.Meter{
|
||||
zeustypes.NewMeter(MeterName, 1, zeustypes.MeterUnitCount, zeustypes.MeterAggregationMax, window, map[string]string{
|
||||
zeustypes.MeterDimensionOrganizationID: orgID.StringValue(),
|
||||
}),
|
||||
}, readings)
|
||||
}
|
||||
|
||||
func TestCollectSkipsNilLicense(t *testing.T) {
|
||||
readings, err := New(&fakeLicensing{}).Collect(context.Background(), valuer.GenerateUUID(), completedWindow())
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, readings)
|
||||
}
|
||||
|
||||
func TestProviderMetadata(t *testing.T) {
|
||||
provider := New(&fakeLicensing{})
|
||||
|
||||
require.Equal(t, "signoz.meter.base.platform.fee", provider.Name().String())
|
||||
require.Equal(t, zeustypes.MeterUnitCount, provider.Unit())
|
||||
require.Equal(t, zeustypes.MeterAggregationMax, provider.Aggregation())
|
||||
}
|
||||
|
||||
func completedWindow() zeustypes.MeterWindow {
|
||||
start := time.Date(2026, 5, 4, 0, 0, 0, 0, time.UTC)
|
||||
return zeustypes.MustNewMeterWindow(start.UnixMilli(), start.AddDate(0, 0, 1).UnixMilli(), true)
|
||||
}
|
||||
|
||||
var _ licensing.Licensing = (*fakeLicensing)(nil)
|
||||
|
||||
type fakeLicensing struct {
|
||||
license *licensetypes.License
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakeLicensing) Start(context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeLicensing) Stop(context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeLicensing) Validate(context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeLicensing) Activate(context.Context, valuer.UUID, string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeLicensing) GetActive(context.Context, valuer.UUID) (*licensetypes.License, error) {
|
||||
return f.license, f.err
|
||||
}
|
||||
|
||||
func (f *fakeLicensing) Refresh(context.Context, valuer.UUID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeLicensing) Checkout(context.Context, valuer.UUID, *licensetypes.PostableSubscription) (*licensetypes.GettableSubscription, error) {
|
||||
return &licensetypes.GettableSubscription{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeLicensing) Portal(context.Context, valuer.UUID, *licensetypes.PostableSubscription) (*licensetypes.GettableSubscription, error) {
|
||||
return &licensetypes.GettableSubscription{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeLicensing) GetFeatureFlags(context.Context, valuer.UUID) ([]*licensetypes.Feature, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeLicensing) Collect(context.Context, valuer.UUID) (map[string]any, error) {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
271
ee/metercollector/datapointcountmetercollector/provider.go
Normal file
271
ee/metercollector/datapointcountmetercollector/provider.go
Normal file
@@ -0,0 +1,271 @@
|
||||
// Package datapointcountmetercollector collects metric datapoint count meters
|
||||
// by workspace and retention. Keep the query local to this meter.
|
||||
package datapointcountmetercollector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/metercollector"
|
||||
"github.com/SigNoz/signoz/pkg/modules/retention"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrymeter"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrymetrics"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/types/retentiontypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/zeustypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// MeterName is the typed registry key for this collector.
|
||||
var (
|
||||
MeterName = zeustypes.MustNewMeterName("signoz.meter.metric.datapoint.count")
|
||||
meterUnit = zeustypes.MeterUnitCount
|
||||
meterAggregation = zeustypes.MeterAggregationSum
|
||||
)
|
||||
|
||||
var _ metercollector.MeterCollector = (*Provider)(nil)
|
||||
|
||||
// Provider collects datapoint count meters.
|
||||
type Provider struct {
|
||||
telemetryStore telemetrystore.TelemetryStore
|
||||
retentionGetter retention.Getter
|
||||
}
|
||||
|
||||
func New(telemetryStore telemetrystore.TelemetryStore, retentionGetter retention.Getter) *Provider {
|
||||
return &Provider{
|
||||
telemetryStore: telemetryStore,
|
||||
retentionGetter: retentionGetter,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) Name() zeustypes.MeterName { return MeterName }
|
||||
func (p *Provider) Unit() zeustypes.MeterUnit { return meterUnit }
|
||||
func (p *Provider) Aggregation() zeustypes.MeterAggregation {
|
||||
return meterAggregation
|
||||
}
|
||||
|
||||
// Collect aggregates datapoint count for the window and emits an empty-day sentinel.
|
||||
func (p *Provider) Collect(ctx context.Context, orgID valuer.UUID, window zeustypes.MeterWindow) ([]zeustypes.Meter, error) {
|
||||
meterName := MeterName.String()
|
||||
|
||||
slices, err := p.retentionGetter.ActiveSlices(
|
||||
ctx,
|
||||
orgID,
|
||||
telemetrymetrics.DBName,
|
||||
telemetrymetrics.SamplesV4LocalTableName,
|
||||
retentiontypes.DefaultMetricsRetentionDays,
|
||||
window.StartUnixMilli,
|
||||
window.EndUnixMilli,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "load retention slices for meter %q", meterName)
|
||||
}
|
||||
|
||||
type bucket struct {
|
||||
dimensions map[string]string
|
||||
value float64
|
||||
}
|
||||
accumulator := make(map[string]*bucket)
|
||||
|
||||
for _, slice := range slices {
|
||||
query, args, dimensionColumns, err := buildQuery(meterName, slice, p.retentionGetter)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "build retention query for meter %q", meterName)
|
||||
}
|
||||
|
||||
rows, err := p.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "query meter %q slice [%d, %d)", meterName, slice.StartMs, slice.EndMs)
|
||||
}
|
||||
|
||||
if err := func() error {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
dimensionValues := make([]string, len(dimensionColumns))
|
||||
var retentionDays int32
|
||||
var retentionRuleIndex int32
|
||||
var value float64
|
||||
|
||||
scanDest := make([]any, 0, len(dimensionValues)+3)
|
||||
for i := range dimensionValues {
|
||||
scanDest = append(scanDest, &dimensionValues[i])
|
||||
}
|
||||
scanDest = append(scanDest, &retentionDays, &retentionRuleIndex, &value)
|
||||
|
||||
if err := rows.Scan(scanDest...); err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "scan meter %q slice [%d, %d)", meterName, slice.StartMs, slice.EndMs)
|
||||
}
|
||||
|
||||
dimensions, err := buildDimensions(orgID, int(retentionDays), int(retentionRuleIndex), dimensionColumns, dimensionValues, slice.Rules)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "build dimensions for meter %q slice [%d, %d)", meterName, slice.StartMs, slice.EndMs)
|
||||
}
|
||||
|
||||
key := bucketKey(dimensions)
|
||||
b, ok := accumulator[key]
|
||||
if !ok {
|
||||
b = &bucket{dimensions: dimensions}
|
||||
accumulator[key] = b
|
||||
}
|
||||
b.value += value
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "iterate meter %q slice [%d, %d)", meterName, slice.StartMs, slice.EndMs)
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
meters := make([]zeustypes.Meter, 0, len(accumulator))
|
||||
for _, b := range accumulator {
|
||||
meters = append(meters, zeustypes.NewMeter(MeterName, b.value, meterUnit, meterAggregation, window, b.dimensions))
|
||||
}
|
||||
|
||||
// Empty windows still emit a sentinel so checkpoints can advance.
|
||||
if len(meters) == 0 && len(slices) > 0 {
|
||||
meters = append(meters, zeustypes.NewMeter(MeterName, 0, meterUnit, meterAggregation, window, map[string]string{
|
||||
zeustypes.MeterDimensionOrganizationID: orgID.StringValue(),
|
||||
zeustypes.MeterDimensionRetentionDays: strconv.Itoa(slices[len(slices)-1].DefaultDays),
|
||||
}))
|
||||
}
|
||||
|
||||
return meters, nil
|
||||
}
|
||||
|
||||
// buildQuery stays local because each meter owns its billing query.
|
||||
func buildQuery(meterName string, slice retentiontypes.Slice, retentionGetter retention.Getter) (string, []any, []dimensionColumn, error) {
|
||||
retentionExpr, err := retentionGetter.BuildMultiIfSQL(slice.Rules, slice.DefaultDays)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
retentionRuleIndexExpr, err := retentionGetter.BuildRuleIndexSQL(slice.Rules)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
columns, err := dimensionColumnsFor(retentionGetter, slice.Rules)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
selects := make([]string, 0, len(columns)+3)
|
||||
groupBy := make([]string, 0, len(columns)+2)
|
||||
for _, column := range columns {
|
||||
selects = append(selects, fmt.Sprintf("JSONExtractString(labels, '%s') AS %s", column.key, column.alias))
|
||||
groupBy = append(groupBy, column.alias)
|
||||
}
|
||||
selects = append(selects,
|
||||
retentionExpr+" AS retention_days",
|
||||
retentionRuleIndexExpr+" AS retention_rule_index",
|
||||
"ifNull(sum(value), 0) AS value",
|
||||
)
|
||||
groupBy = append(groupBy, "retention_days", "retention_rule_index")
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
sb.Select(selects...)
|
||||
sb.From(telemetrymeter.DBName + "." + telemetrymeter.SamplesTableName)
|
||||
sb.Where(
|
||||
sb.Equal("metric_name", meterName),
|
||||
sb.GTE("unix_milli", slice.StartMs),
|
||||
sb.LT("unix_milli", slice.EndMs),
|
||||
)
|
||||
sb.GroupBy(groupBy...)
|
||||
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
return query, args, columns, nil
|
||||
}
|
||||
|
||||
type dimensionColumn struct {
|
||||
key string
|
||||
alias string
|
||||
}
|
||||
|
||||
func dimensionColumnsFor(retentionGetter retention.Getter, rules []retentiontypes.CustomRetentionRule) ([]dimensionColumn, error) {
|
||||
dimensionKeys, err := retentionGetter.RuleDimensionKeys(rules)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys := make([]string, 0, len(dimensionKeys)+1)
|
||||
keys = append(keys, zeustypes.MeterDimensionWorkspaceKeyID)
|
||||
for _, key := range dimensionKeys {
|
||||
if key == zeustypes.MeterDimensionWorkspaceKeyID {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, key)
|
||||
}
|
||||
columns := make([]dimensionColumn, len(keys))
|
||||
for i, key := range keys {
|
||||
columns[i] = dimensionColumn{key: key, alias: fmt.Sprintf("dim_%d", i)}
|
||||
}
|
||||
return columns, nil
|
||||
}
|
||||
|
||||
func buildDimensions(
|
||||
orgID valuer.UUID,
|
||||
retentionDays int,
|
||||
retentionRuleIndex int,
|
||||
columns []dimensionColumn,
|
||||
values []string,
|
||||
rules []retentiontypes.CustomRetentionRule,
|
||||
) (map[string]string, error) {
|
||||
if len(columns) != len(values) {
|
||||
return nil, errors.Newf(errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "dimension column/value count mismatch: %d columns, %d values", len(columns), len(values))
|
||||
}
|
||||
|
||||
valuesByKey := make(map[string]string, len(columns))
|
||||
for i, column := range columns {
|
||||
valuesByKey[column.key] = values[i]
|
||||
}
|
||||
|
||||
dimensions := map[string]string{
|
||||
zeustypes.MeterDimensionOrganizationID: orgID.StringValue(),
|
||||
zeustypes.MeterDimensionRetentionDays: strconv.Itoa(retentionDays),
|
||||
}
|
||||
addNonEmpty(dimensions, zeustypes.MeterDimensionWorkspaceKeyID, valuesByKey[zeustypes.MeterDimensionWorkspaceKeyID])
|
||||
|
||||
if retentionRuleIndex < 0 {
|
||||
return dimensions, nil
|
||||
}
|
||||
if retentionRuleIndex >= len(rules) {
|
||||
return nil, errors.Newf(errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "retention rule index %d out of range for %d rules", retentionRuleIndex, len(rules))
|
||||
}
|
||||
for _, filter := range rules[retentionRuleIndex].Filters {
|
||||
addNonEmpty(dimensions, filter.Key, valuesByKey[filter.Key])
|
||||
}
|
||||
return dimensions, nil
|
||||
}
|
||||
|
||||
func addNonEmpty(dimensions map[string]string, key, value string) {
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
dimensions[key] = value
|
||||
}
|
||||
|
||||
func bucketKey(dimensions map[string]string) string {
|
||||
keys := make([]string, 0, len(dimensions))
|
||||
for key := range dimensions {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var b strings.Builder
|
||||
for _, key := range keys {
|
||||
value := dimensions[key]
|
||||
b.WriteString(strconv.Itoa(len(key)))
|
||||
b.WriteByte(':')
|
||||
b.WriteString(key)
|
||||
b.WriteByte('=')
|
||||
b.WriteString(strconv.Itoa(len(value)))
|
||||
b.WriteByte(':')
|
||||
b.WriteString(value)
|
||||
b.WriteByte(';')
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package datapointcountmetercollector
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/retentiontypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/zeustypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBuildDimensions(t *testing.T) {
|
||||
orgID := valuer.GenerateUUID()
|
||||
rules := []retentiontypes.CustomRetentionRule{{
|
||||
Filters: []retentiontypes.FilterCondition{{
|
||||
Key: "service.name",
|
||||
Values: []string{"api"},
|
||||
}},
|
||||
TTLDays: 7,
|
||||
}}
|
||||
columns := []dimensionColumn{
|
||||
{key: zeustypes.MeterDimensionWorkspaceKeyID, alias: "dim_0"},
|
||||
{key: "service.name", alias: "dim_1"},
|
||||
}
|
||||
|
||||
dimensions, err := buildDimensions(orgID, 30, 0, columns, []string{"workspace-1", "api"}, rules)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]string{
|
||||
zeustypes.MeterDimensionOrganizationID: orgID.StringValue(),
|
||||
zeustypes.MeterDimensionRetentionDays: "30",
|
||||
zeustypes.MeterDimensionWorkspaceKeyID: "workspace-1",
|
||||
"service.name": "api",
|
||||
}, dimensions)
|
||||
}
|
||||
|
||||
func TestProviderMetadata(t *testing.T) {
|
||||
provider := New(nil, nil)
|
||||
|
||||
require.Equal(t, "signoz.meter.metric.datapoint.count", provider.Name().String())
|
||||
require.Equal(t, zeustypes.MeterUnitCount, provider.Unit())
|
||||
require.Equal(t, zeustypes.MeterAggregationSum, provider.Aggregation())
|
||||
}
|
||||
|
||||
func TestBucketKeyIsStable(t *testing.T) {
|
||||
first := bucketKey(map[string]string{
|
||||
"service.name": "api",
|
||||
zeustypes.MeterDimensionRetentionDays: "30",
|
||||
zeustypes.MeterDimensionWorkspaceKeyID: "workspace-1",
|
||||
})
|
||||
second := bucketKey(map[string]string{
|
||||
zeustypes.MeterDimensionWorkspaceKeyID: "workspace-1",
|
||||
"service.name": "api",
|
||||
zeustypes.MeterDimensionRetentionDays: "30",
|
||||
})
|
||||
|
||||
require.Equal(t, first, second)
|
||||
require.NotEmpty(t, first)
|
||||
}
|
||||
271
ee/metercollector/datapointsizemetercollector/provider.go
Normal file
271
ee/metercollector/datapointsizemetercollector/provider.go
Normal file
@@ -0,0 +1,271 @@
|
||||
// Package datapointsizemetercollector collects metric datapoint size meters
|
||||
// by workspace and retention. Keep the query local to this meter.
|
||||
package datapointsizemetercollector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/metercollector"
|
||||
"github.com/SigNoz/signoz/pkg/modules/retention"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrymeter"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrymetrics"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/types/retentiontypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/zeustypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// MeterName is the typed registry key for this collector.
|
||||
var (
|
||||
MeterName = zeustypes.MustNewMeterName("signoz.meter.metric.datapoint.size")
|
||||
meterUnit = zeustypes.MeterUnitBytes
|
||||
meterAggregation = zeustypes.MeterAggregationSum
|
||||
)
|
||||
|
||||
var _ metercollector.MeterCollector = (*Provider)(nil)
|
||||
|
||||
// Provider collects datapoint size meters.
|
||||
type Provider struct {
|
||||
telemetryStore telemetrystore.TelemetryStore
|
||||
retentionGetter retention.Getter
|
||||
}
|
||||
|
||||
func New(telemetryStore telemetrystore.TelemetryStore, retentionGetter retention.Getter) *Provider {
|
||||
return &Provider{
|
||||
telemetryStore: telemetryStore,
|
||||
retentionGetter: retentionGetter,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) Name() zeustypes.MeterName { return MeterName }
|
||||
func (p *Provider) Unit() zeustypes.MeterUnit { return meterUnit }
|
||||
func (p *Provider) Aggregation() zeustypes.MeterAggregation {
|
||||
return meterAggregation
|
||||
}
|
||||
|
||||
// Collect aggregates datapoint size for the window and emits an empty-day sentinel.
|
||||
func (p *Provider) Collect(ctx context.Context, orgID valuer.UUID, window zeustypes.MeterWindow) ([]zeustypes.Meter, error) {
|
||||
meterName := MeterName.String()
|
||||
|
||||
slices, err := p.retentionGetter.ActiveSlices(
|
||||
ctx,
|
||||
orgID,
|
||||
telemetrymetrics.DBName,
|
||||
telemetrymetrics.SamplesV4LocalTableName,
|
||||
retentiontypes.DefaultMetricsRetentionDays,
|
||||
window.StartUnixMilli,
|
||||
window.EndUnixMilli,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "load retention slices for meter %q", meterName)
|
||||
}
|
||||
|
||||
type bucket struct {
|
||||
dimensions map[string]string
|
||||
value float64
|
||||
}
|
||||
accumulator := make(map[string]*bucket)
|
||||
|
||||
for _, slice := range slices {
|
||||
query, args, dimensionColumns, err := buildQuery(meterName, slice, p.retentionGetter)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "build retention query for meter %q", meterName)
|
||||
}
|
||||
|
||||
rows, err := p.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "query meter %q slice [%d, %d)", meterName, slice.StartMs, slice.EndMs)
|
||||
}
|
||||
|
||||
if err := func() error {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
dimensionValues := make([]string, len(dimensionColumns))
|
||||
var retentionDays int32
|
||||
var retentionRuleIndex int32
|
||||
var value float64
|
||||
|
||||
scanDest := make([]any, 0, len(dimensionValues)+3)
|
||||
for i := range dimensionValues {
|
||||
scanDest = append(scanDest, &dimensionValues[i])
|
||||
}
|
||||
scanDest = append(scanDest, &retentionDays, &retentionRuleIndex, &value)
|
||||
|
||||
if err := rows.Scan(scanDest...); err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "scan meter %q slice [%d, %d)", meterName, slice.StartMs, slice.EndMs)
|
||||
}
|
||||
|
||||
dimensions, err := buildDimensions(orgID, int(retentionDays), int(retentionRuleIndex), dimensionColumns, dimensionValues, slice.Rules)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "build dimensions for meter %q slice [%d, %d)", meterName, slice.StartMs, slice.EndMs)
|
||||
}
|
||||
|
||||
key := bucketKey(dimensions)
|
||||
b, ok := accumulator[key]
|
||||
if !ok {
|
||||
b = &bucket{dimensions: dimensions}
|
||||
accumulator[key] = b
|
||||
}
|
||||
b.value += value
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "iterate meter %q slice [%d, %d)", meterName, slice.StartMs, slice.EndMs)
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
meters := make([]zeustypes.Meter, 0, len(accumulator))
|
||||
for _, b := range accumulator {
|
||||
meters = append(meters, zeustypes.NewMeter(MeterName, b.value, meterUnit, meterAggregation, window, b.dimensions))
|
||||
}
|
||||
|
||||
// Empty windows still emit a sentinel so checkpoints can advance.
|
||||
if len(meters) == 0 && len(slices) > 0 {
|
||||
meters = append(meters, zeustypes.NewMeter(MeterName, 0, meterUnit, meterAggregation, window, map[string]string{
|
||||
zeustypes.MeterDimensionOrganizationID: orgID.StringValue(),
|
||||
zeustypes.MeterDimensionRetentionDays: strconv.Itoa(slices[len(slices)-1].DefaultDays),
|
||||
}))
|
||||
}
|
||||
|
||||
return meters, nil
|
||||
}
|
||||
|
||||
// buildQuery stays local because each meter owns its billing query.
|
||||
func buildQuery(meterName string, slice retentiontypes.Slice, retentionGetter retention.Getter) (string, []any, []dimensionColumn, error) {
|
||||
retentionExpr, err := retentionGetter.BuildMultiIfSQL(slice.Rules, slice.DefaultDays)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
retentionRuleIndexExpr, err := retentionGetter.BuildRuleIndexSQL(slice.Rules)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
columns, err := dimensionColumnsFor(retentionGetter, slice.Rules)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
selects := make([]string, 0, len(columns)+3)
|
||||
groupBy := make([]string, 0, len(columns)+2)
|
||||
for _, column := range columns {
|
||||
selects = append(selects, fmt.Sprintf("JSONExtractString(labels, '%s') AS %s", column.key, column.alias))
|
||||
groupBy = append(groupBy, column.alias)
|
||||
}
|
||||
selects = append(selects,
|
||||
retentionExpr+" AS retention_days",
|
||||
retentionRuleIndexExpr+" AS retention_rule_index",
|
||||
"ifNull(sum(value), 0) AS value",
|
||||
)
|
||||
groupBy = append(groupBy, "retention_days", "retention_rule_index")
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
sb.Select(selects...)
|
||||
sb.From(telemetrymeter.DBName + "." + telemetrymeter.SamplesTableName)
|
||||
sb.Where(
|
||||
sb.Equal("metric_name", meterName),
|
||||
sb.GTE("unix_milli", slice.StartMs),
|
||||
sb.LT("unix_milli", slice.EndMs),
|
||||
)
|
||||
sb.GroupBy(groupBy...)
|
||||
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
return query, args, columns, nil
|
||||
}
|
||||
|
||||
type dimensionColumn struct {
|
||||
key string
|
||||
alias string
|
||||
}
|
||||
|
||||
func dimensionColumnsFor(retentionGetter retention.Getter, rules []retentiontypes.CustomRetentionRule) ([]dimensionColumn, error) {
|
||||
dimensionKeys, err := retentionGetter.RuleDimensionKeys(rules)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys := make([]string, 0, len(dimensionKeys)+1)
|
||||
keys = append(keys, zeustypes.MeterDimensionWorkspaceKeyID)
|
||||
for _, key := range dimensionKeys {
|
||||
if key == zeustypes.MeterDimensionWorkspaceKeyID {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, key)
|
||||
}
|
||||
columns := make([]dimensionColumn, len(keys))
|
||||
for i, key := range keys {
|
||||
columns[i] = dimensionColumn{key: key, alias: fmt.Sprintf("dim_%d", i)}
|
||||
}
|
||||
return columns, nil
|
||||
}
|
||||
|
||||
func buildDimensions(
|
||||
orgID valuer.UUID,
|
||||
retentionDays int,
|
||||
retentionRuleIndex int,
|
||||
columns []dimensionColumn,
|
||||
values []string,
|
||||
rules []retentiontypes.CustomRetentionRule,
|
||||
) (map[string]string, error) {
|
||||
if len(columns) != len(values) {
|
||||
return nil, errors.Newf(errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "dimension column/value count mismatch: %d columns, %d values", len(columns), len(values))
|
||||
}
|
||||
|
||||
valuesByKey := make(map[string]string, len(columns))
|
||||
for i, column := range columns {
|
||||
valuesByKey[column.key] = values[i]
|
||||
}
|
||||
|
||||
dimensions := map[string]string{
|
||||
zeustypes.MeterDimensionOrganizationID: orgID.StringValue(),
|
||||
zeustypes.MeterDimensionRetentionDays: strconv.Itoa(retentionDays),
|
||||
}
|
||||
addNonEmpty(dimensions, zeustypes.MeterDimensionWorkspaceKeyID, valuesByKey[zeustypes.MeterDimensionWorkspaceKeyID])
|
||||
|
||||
if retentionRuleIndex < 0 {
|
||||
return dimensions, nil
|
||||
}
|
||||
if retentionRuleIndex >= len(rules) {
|
||||
return nil, errors.Newf(errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "retention rule index %d out of range for %d rules", retentionRuleIndex, len(rules))
|
||||
}
|
||||
for _, filter := range rules[retentionRuleIndex].Filters {
|
||||
addNonEmpty(dimensions, filter.Key, valuesByKey[filter.Key])
|
||||
}
|
||||
return dimensions, nil
|
||||
}
|
||||
|
||||
func addNonEmpty(dimensions map[string]string, key, value string) {
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
dimensions[key] = value
|
||||
}
|
||||
|
||||
func bucketKey(dimensions map[string]string) string {
|
||||
keys := make([]string, 0, len(dimensions))
|
||||
for key := range dimensions {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var b strings.Builder
|
||||
for _, key := range keys {
|
||||
value := dimensions[key]
|
||||
b.WriteString(strconv.Itoa(len(key)))
|
||||
b.WriteByte(':')
|
||||
b.WriteString(key)
|
||||
b.WriteByte('=')
|
||||
b.WriteString(strconv.Itoa(len(value)))
|
||||
b.WriteByte(':')
|
||||
b.WriteString(value)
|
||||
b.WriteByte(';')
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package datapointsizemetercollector
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/retentiontypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/zeustypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBuildDimensions(t *testing.T) {
|
||||
orgID := valuer.GenerateUUID()
|
||||
rules := []retentiontypes.CustomRetentionRule{{
|
||||
Filters: []retentiontypes.FilterCondition{{
|
||||
Key: "service.name",
|
||||
Values: []string{"api"},
|
||||
}},
|
||||
TTLDays: 7,
|
||||
}}
|
||||
columns := []dimensionColumn{
|
||||
{key: zeustypes.MeterDimensionWorkspaceKeyID, alias: "dim_0"},
|
||||
{key: "service.name", alias: "dim_1"},
|
||||
}
|
||||
|
||||
dimensions, err := buildDimensions(orgID, 30, 0, columns, []string{"workspace-1", "api"}, rules)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]string{
|
||||
zeustypes.MeterDimensionOrganizationID: orgID.StringValue(),
|
||||
zeustypes.MeterDimensionRetentionDays: "30",
|
||||
zeustypes.MeterDimensionWorkspaceKeyID: "workspace-1",
|
||||
"service.name": "api",
|
||||
}, dimensions)
|
||||
}
|
||||
|
||||
func TestProviderMetadata(t *testing.T) {
|
||||
provider := New(nil, nil)
|
||||
|
||||
require.Equal(t, "signoz.meter.metric.datapoint.size", provider.Name().String())
|
||||
require.Equal(t, zeustypes.MeterUnitBytes, provider.Unit())
|
||||
require.Equal(t, zeustypes.MeterAggregationSum, provider.Aggregation())
|
||||
}
|
||||
|
||||
func TestBucketKeyIsStable(t *testing.T) {
|
||||
first := bucketKey(map[string]string{
|
||||
"service.name": "api",
|
||||
zeustypes.MeterDimensionRetentionDays: "30",
|
||||
zeustypes.MeterDimensionWorkspaceKeyID: "workspace-1",
|
||||
})
|
||||
second := bucketKey(map[string]string{
|
||||
zeustypes.MeterDimensionWorkspaceKeyID: "workspace-1",
|
||||
"service.name": "api",
|
||||
zeustypes.MeterDimensionRetentionDays: "30",
|
||||
})
|
||||
|
||||
require.Equal(t, first, second)
|
||||
require.NotEmpty(t, first)
|
||||
}
|
||||
271
ee/metercollector/logcountmetercollector/provider.go
Normal file
271
ee/metercollector/logcountmetercollector/provider.go
Normal file
@@ -0,0 +1,271 @@
|
||||
// Package logcountmetercollector collects log count meters by workspace and
|
||||
// retention. Keep the query local to this meter.
|
||||
package logcountmetercollector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/metercollector"
|
||||
"github.com/SigNoz/signoz/pkg/modules/retention"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrylogs"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrymeter"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/types/retentiontypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/zeustypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// MeterName is the typed registry key for this collector.
|
||||
var (
|
||||
MeterName = zeustypes.MustNewMeterName("signoz.meter.log.count")
|
||||
meterUnit = zeustypes.MeterUnitCount
|
||||
meterAggregation = zeustypes.MeterAggregationSum
|
||||
)
|
||||
|
||||
var _ metercollector.MeterCollector = (*Provider)(nil)
|
||||
|
||||
// Provider collects log count meters.
|
||||
type Provider struct {
|
||||
telemetryStore telemetrystore.TelemetryStore
|
||||
retentionGetter retention.Getter
|
||||
}
|
||||
|
||||
func New(telemetryStore telemetrystore.TelemetryStore, retentionGetter retention.Getter) *Provider {
|
||||
return &Provider{
|
||||
telemetryStore: telemetryStore,
|
||||
retentionGetter: retentionGetter,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) Name() zeustypes.MeterName { return MeterName }
|
||||
func (p *Provider) Unit() zeustypes.MeterUnit { return meterUnit }
|
||||
func (p *Provider) Aggregation() zeustypes.MeterAggregation {
|
||||
return meterAggregation
|
||||
}
|
||||
|
||||
// Collect aggregates log count for the window and emits an empty-day sentinel.
|
||||
func (p *Provider) Collect(ctx context.Context, orgID valuer.UUID, window zeustypes.MeterWindow) ([]zeustypes.Meter, error) {
|
||||
meterName := MeterName.String()
|
||||
|
||||
slices, err := p.retentionGetter.ActiveSlices(
|
||||
ctx,
|
||||
orgID,
|
||||
telemetrylogs.DBName,
|
||||
telemetrylogs.LogsV2LocalTableName,
|
||||
retentiontypes.DefaultLogsRetentionDays,
|
||||
window.StartUnixMilli,
|
||||
window.EndUnixMilli,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "load retention slices for meter %q", meterName)
|
||||
}
|
||||
|
||||
type bucket struct {
|
||||
dimensions map[string]string
|
||||
value float64
|
||||
}
|
||||
accumulator := make(map[string]*bucket)
|
||||
|
||||
for _, slice := range slices {
|
||||
query, args, dimensionColumns, err := buildQuery(meterName, slice, p.retentionGetter)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "build retention query for meter %q", meterName)
|
||||
}
|
||||
|
||||
rows, err := p.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "query meter %q slice [%d, %d)", meterName, slice.StartMs, slice.EndMs)
|
||||
}
|
||||
|
||||
if err := func() error {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
dimensionValues := make([]string, len(dimensionColumns))
|
||||
var retentionDays int32
|
||||
var retentionRuleIndex int32
|
||||
var value float64
|
||||
|
||||
scanDest := make([]any, 0, len(dimensionValues)+3)
|
||||
for i := range dimensionValues {
|
||||
scanDest = append(scanDest, &dimensionValues[i])
|
||||
}
|
||||
scanDest = append(scanDest, &retentionDays, &retentionRuleIndex, &value)
|
||||
|
||||
if err := rows.Scan(scanDest...); err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "scan meter %q slice [%d, %d)", meterName, slice.StartMs, slice.EndMs)
|
||||
}
|
||||
|
||||
dimensions, err := buildDimensions(orgID, int(retentionDays), int(retentionRuleIndex), dimensionColumns, dimensionValues, slice.Rules)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "build dimensions for meter %q slice [%d, %d)", meterName, slice.StartMs, slice.EndMs)
|
||||
}
|
||||
|
||||
key := bucketKey(dimensions)
|
||||
b, ok := accumulator[key]
|
||||
if !ok {
|
||||
b = &bucket{dimensions: dimensions}
|
||||
accumulator[key] = b
|
||||
}
|
||||
b.value += value
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "iterate meter %q slice [%d, %d)", meterName, slice.StartMs, slice.EndMs)
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
meters := make([]zeustypes.Meter, 0, len(accumulator))
|
||||
for _, b := range accumulator {
|
||||
meters = append(meters, zeustypes.NewMeter(MeterName, b.value, meterUnit, meterAggregation, window, b.dimensions))
|
||||
}
|
||||
|
||||
// Empty windows still emit a sentinel so checkpoints can advance.
|
||||
if len(meters) == 0 && len(slices) > 0 {
|
||||
meters = append(meters, zeustypes.NewMeter(MeterName, 0, meterUnit, meterAggregation, window, map[string]string{
|
||||
zeustypes.MeterDimensionOrganizationID: orgID.StringValue(),
|
||||
zeustypes.MeterDimensionRetentionDays: strconv.Itoa(slices[len(slices)-1].DefaultDays),
|
||||
}))
|
||||
}
|
||||
|
||||
return meters, nil
|
||||
}
|
||||
|
||||
// buildQuery stays local because each meter owns its billing query.
|
||||
func buildQuery(meterName string, slice retentiontypes.Slice, retentionGetter retention.Getter) (string, []any, []dimensionColumn, error) {
|
||||
retentionExpr, err := retentionGetter.BuildMultiIfSQL(slice.Rules, slice.DefaultDays)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
retentionRuleIndexExpr, err := retentionGetter.BuildRuleIndexSQL(slice.Rules)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
columns, err := dimensionColumnsFor(retentionGetter, slice.Rules)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
selects := make([]string, 0, len(columns)+3)
|
||||
groupBy := make([]string, 0, len(columns)+2)
|
||||
for _, column := range columns {
|
||||
selects = append(selects, fmt.Sprintf("JSONExtractString(labels, '%s') AS %s", column.key, column.alias))
|
||||
groupBy = append(groupBy, column.alias)
|
||||
}
|
||||
selects = append(selects,
|
||||
retentionExpr+" AS retention_days",
|
||||
retentionRuleIndexExpr+" AS retention_rule_index",
|
||||
"ifNull(sum(value), 0) AS value",
|
||||
)
|
||||
groupBy = append(groupBy, "retention_days", "retention_rule_index")
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
sb.Select(selects...)
|
||||
sb.From(telemetrymeter.DBName + "." + telemetrymeter.SamplesTableName)
|
||||
sb.Where(
|
||||
sb.Equal("metric_name", meterName),
|
||||
sb.GTE("unix_milli", slice.StartMs),
|
||||
sb.LT("unix_milli", slice.EndMs),
|
||||
)
|
||||
sb.GroupBy(groupBy...)
|
||||
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
return query, args, columns, nil
|
||||
}
|
||||
|
||||
type dimensionColumn struct {
|
||||
key string
|
||||
alias string
|
||||
}
|
||||
|
||||
func dimensionColumnsFor(retentionGetter retention.Getter, rules []retentiontypes.CustomRetentionRule) ([]dimensionColumn, error) {
|
||||
dimensionKeys, err := retentionGetter.RuleDimensionKeys(rules)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys := make([]string, 0, len(dimensionKeys)+1)
|
||||
keys = append(keys, zeustypes.MeterDimensionWorkspaceKeyID)
|
||||
for _, key := range dimensionKeys {
|
||||
if key == zeustypes.MeterDimensionWorkspaceKeyID {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, key)
|
||||
}
|
||||
columns := make([]dimensionColumn, len(keys))
|
||||
for i, key := range keys {
|
||||
columns[i] = dimensionColumn{key: key, alias: fmt.Sprintf("dim_%d", i)}
|
||||
}
|
||||
return columns, nil
|
||||
}
|
||||
|
||||
func buildDimensions(
|
||||
orgID valuer.UUID,
|
||||
retentionDays int,
|
||||
retentionRuleIndex int,
|
||||
columns []dimensionColumn,
|
||||
values []string,
|
||||
rules []retentiontypes.CustomRetentionRule,
|
||||
) (map[string]string, error) {
|
||||
if len(columns) != len(values) {
|
||||
return nil, errors.Newf(errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "dimension column/value count mismatch: %d columns, %d values", len(columns), len(values))
|
||||
}
|
||||
|
||||
valuesByKey := make(map[string]string, len(columns))
|
||||
for i, column := range columns {
|
||||
valuesByKey[column.key] = values[i]
|
||||
}
|
||||
|
||||
dimensions := map[string]string{
|
||||
zeustypes.MeterDimensionOrganizationID: orgID.StringValue(),
|
||||
zeustypes.MeterDimensionRetentionDays: strconv.Itoa(retentionDays),
|
||||
}
|
||||
addNonEmpty(dimensions, zeustypes.MeterDimensionWorkspaceKeyID, valuesByKey[zeustypes.MeterDimensionWorkspaceKeyID])
|
||||
|
||||
if retentionRuleIndex < 0 {
|
||||
return dimensions, nil
|
||||
}
|
||||
if retentionRuleIndex >= len(rules) {
|
||||
return nil, errors.Newf(errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "retention rule index %d out of range for %d rules", retentionRuleIndex, len(rules))
|
||||
}
|
||||
for _, filter := range rules[retentionRuleIndex].Filters {
|
||||
addNonEmpty(dimensions, filter.Key, valuesByKey[filter.Key])
|
||||
}
|
||||
return dimensions, nil
|
||||
}
|
||||
|
||||
func addNonEmpty(dimensions map[string]string, key, value string) {
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
dimensions[key] = value
|
||||
}
|
||||
|
||||
func bucketKey(dimensions map[string]string) string {
|
||||
keys := make([]string, 0, len(dimensions))
|
||||
for key := range dimensions {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var b strings.Builder
|
||||
for _, key := range keys {
|
||||
value := dimensions[key]
|
||||
b.WriteString(strconv.Itoa(len(key)))
|
||||
b.WriteByte(':')
|
||||
b.WriteString(key)
|
||||
b.WriteByte('=')
|
||||
b.WriteString(strconv.Itoa(len(value)))
|
||||
b.WriteByte(':')
|
||||
b.WriteString(value)
|
||||
b.WriteByte(';')
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
58
ee/metercollector/logcountmetercollector/provider_test.go
Normal file
58
ee/metercollector/logcountmetercollector/provider_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package logcountmetercollector
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/retentiontypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/zeustypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBuildDimensions(t *testing.T) {
|
||||
orgID := valuer.GenerateUUID()
|
||||
rules := []retentiontypes.CustomRetentionRule{{
|
||||
Filters: []retentiontypes.FilterCondition{{
|
||||
Key: "service.name",
|
||||
Values: []string{"api"},
|
||||
}},
|
||||
TTLDays: 7,
|
||||
}}
|
||||
columns := []dimensionColumn{
|
||||
{key: zeustypes.MeterDimensionWorkspaceKeyID, alias: "dim_0"},
|
||||
{key: "service.name", alias: "dim_1"},
|
||||
}
|
||||
|
||||
dimensions, err := buildDimensions(orgID, 30, 0, columns, []string{"workspace-1", "api"}, rules)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]string{
|
||||
zeustypes.MeterDimensionOrganizationID: orgID.StringValue(),
|
||||
zeustypes.MeterDimensionRetentionDays: "30",
|
||||
zeustypes.MeterDimensionWorkspaceKeyID: "workspace-1",
|
||||
"service.name": "api",
|
||||
}, dimensions)
|
||||
}
|
||||
|
||||
func TestProviderMetadata(t *testing.T) {
|
||||
provider := New(nil, nil)
|
||||
|
||||
require.Equal(t, "signoz.meter.log.count", provider.Name().String())
|
||||
require.Equal(t, zeustypes.MeterUnitCount, provider.Unit())
|
||||
require.Equal(t, zeustypes.MeterAggregationSum, provider.Aggregation())
|
||||
}
|
||||
|
||||
func TestBucketKeyIsStable(t *testing.T) {
|
||||
first := bucketKey(map[string]string{
|
||||
"service.name": "api",
|
||||
zeustypes.MeterDimensionRetentionDays: "30",
|
||||
zeustypes.MeterDimensionWorkspaceKeyID: "workspace-1",
|
||||
})
|
||||
second := bucketKey(map[string]string{
|
||||
zeustypes.MeterDimensionWorkspaceKeyID: "workspace-1",
|
||||
"service.name": "api",
|
||||
zeustypes.MeterDimensionRetentionDays: "30",
|
||||
})
|
||||
|
||||
require.Equal(t, first, second)
|
||||
require.NotEmpty(t, first)
|
||||
}
|
||||
271
ee/metercollector/logsizemetercollector/provider.go
Normal file
271
ee/metercollector/logsizemetercollector/provider.go
Normal file
@@ -0,0 +1,271 @@
|
||||
// Package logsizemetercollector collects log size meters by workspace and
|
||||
// retention. Keep the query local to this meter.
|
||||
package logsizemetercollector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/metercollector"
|
||||
"github.com/SigNoz/signoz/pkg/modules/retention"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrylogs"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrymeter"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/types/retentiontypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/zeustypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// MeterName is the typed registry key for this collector.
|
||||
var (
|
||||
MeterName = zeustypes.MustNewMeterName("signoz.meter.log.size")
|
||||
meterUnit = zeustypes.MeterUnitBytes
|
||||
meterAggregation = zeustypes.MeterAggregationSum
|
||||
)
|
||||
|
||||
var _ metercollector.MeterCollector = (*Provider)(nil)
|
||||
|
||||
// Provider collects log size meters.
|
||||
type Provider struct {
|
||||
telemetryStore telemetrystore.TelemetryStore
|
||||
retentionGetter retention.Getter
|
||||
}
|
||||
|
||||
func New(telemetryStore telemetrystore.TelemetryStore, retentionGetter retention.Getter) *Provider {
|
||||
return &Provider{
|
||||
telemetryStore: telemetryStore,
|
||||
retentionGetter: retentionGetter,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) Name() zeustypes.MeterName { return MeterName }
|
||||
func (p *Provider) Unit() zeustypes.MeterUnit { return meterUnit }
|
||||
func (p *Provider) Aggregation() zeustypes.MeterAggregation {
|
||||
return meterAggregation
|
||||
}
|
||||
|
||||
// Collect aggregates log size for the window and emits an empty-day sentinel.
|
||||
func (p *Provider) Collect(ctx context.Context, orgID valuer.UUID, window zeustypes.MeterWindow) ([]zeustypes.Meter, error) {
|
||||
meterName := MeterName.String()
|
||||
|
||||
slices, err := p.retentionGetter.ActiveSlices(
|
||||
ctx,
|
||||
orgID,
|
||||
telemetrylogs.DBName,
|
||||
telemetrylogs.LogsV2LocalTableName,
|
||||
retentiontypes.DefaultLogsRetentionDays,
|
||||
window.StartUnixMilli,
|
||||
window.EndUnixMilli,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "load retention slices for meter %q", meterName)
|
||||
}
|
||||
|
||||
type bucket struct {
|
||||
dimensions map[string]string
|
||||
value float64
|
||||
}
|
||||
accumulator := make(map[string]*bucket)
|
||||
|
||||
for _, slice := range slices {
|
||||
query, args, dimensionColumns, err := buildQuery(meterName, slice, p.retentionGetter)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "build retention query for meter %q", meterName)
|
||||
}
|
||||
|
||||
rows, err := p.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "query meter %q slice [%d, %d)", meterName, slice.StartMs, slice.EndMs)
|
||||
}
|
||||
|
||||
if err := func() error {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
dimensionValues := make([]string, len(dimensionColumns))
|
||||
var retentionDays int32
|
||||
var retentionRuleIndex int32
|
||||
var value float64
|
||||
|
||||
scanDest := make([]any, 0, len(dimensionValues)+3)
|
||||
for i := range dimensionValues {
|
||||
scanDest = append(scanDest, &dimensionValues[i])
|
||||
}
|
||||
scanDest = append(scanDest, &retentionDays, &retentionRuleIndex, &value)
|
||||
|
||||
if err := rows.Scan(scanDest...); err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "scan meter %q slice [%d, %d)", meterName, slice.StartMs, slice.EndMs)
|
||||
}
|
||||
|
||||
dimensions, err := buildDimensions(orgID, int(retentionDays), int(retentionRuleIndex), dimensionColumns, dimensionValues, slice.Rules)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "build dimensions for meter %q slice [%d, %d)", meterName, slice.StartMs, slice.EndMs)
|
||||
}
|
||||
|
||||
key := bucketKey(dimensions)
|
||||
b, ok := accumulator[key]
|
||||
if !ok {
|
||||
b = &bucket{dimensions: dimensions}
|
||||
accumulator[key] = b
|
||||
}
|
||||
b.value += value
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "iterate meter %q slice [%d, %d)", meterName, slice.StartMs, slice.EndMs)
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
meters := make([]zeustypes.Meter, 0, len(accumulator))
|
||||
for _, b := range accumulator {
|
||||
meters = append(meters, zeustypes.NewMeter(MeterName, b.value, meterUnit, meterAggregation, window, b.dimensions))
|
||||
}
|
||||
|
||||
// Empty windows still emit a sentinel so checkpoints can advance.
|
||||
if len(meters) == 0 && len(slices) > 0 {
|
||||
meters = append(meters, zeustypes.NewMeter(MeterName, 0, meterUnit, meterAggregation, window, map[string]string{
|
||||
zeustypes.MeterDimensionOrganizationID: orgID.StringValue(),
|
||||
zeustypes.MeterDimensionRetentionDays: strconv.Itoa(slices[len(slices)-1].DefaultDays),
|
||||
}))
|
||||
}
|
||||
|
||||
return meters, nil
|
||||
}
|
||||
|
||||
// buildQuery stays local because each meter owns its billing query.
|
||||
func buildQuery(meterName string, slice retentiontypes.Slice, retentionGetter retention.Getter) (string, []any, []dimensionColumn, error) {
|
||||
retentionExpr, err := retentionGetter.BuildMultiIfSQL(slice.Rules, slice.DefaultDays)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
retentionRuleIndexExpr, err := retentionGetter.BuildRuleIndexSQL(slice.Rules)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
columns, err := dimensionColumnsFor(retentionGetter, slice.Rules)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
selects := make([]string, 0, len(columns)+3)
|
||||
groupBy := make([]string, 0, len(columns)+2)
|
||||
for _, column := range columns {
|
||||
selects = append(selects, fmt.Sprintf("JSONExtractString(labels, '%s') AS %s", column.key, column.alias))
|
||||
groupBy = append(groupBy, column.alias)
|
||||
}
|
||||
selects = append(selects,
|
||||
retentionExpr+" AS retention_days",
|
||||
retentionRuleIndexExpr+" AS retention_rule_index",
|
||||
"ifNull(sum(value), 0) AS value",
|
||||
)
|
||||
groupBy = append(groupBy, "retention_days", "retention_rule_index")
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
sb.Select(selects...)
|
||||
sb.From(telemetrymeter.DBName + "." + telemetrymeter.SamplesTableName)
|
||||
sb.Where(
|
||||
sb.Equal("metric_name", meterName),
|
||||
sb.GTE("unix_milli", slice.StartMs),
|
||||
sb.LT("unix_milli", slice.EndMs),
|
||||
)
|
||||
sb.GroupBy(groupBy...)
|
||||
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
return query, args, columns, nil
|
||||
}
|
||||
|
||||
type dimensionColumn struct {
|
||||
key string
|
||||
alias string
|
||||
}
|
||||
|
||||
func dimensionColumnsFor(retentionGetter retention.Getter, rules []retentiontypes.CustomRetentionRule) ([]dimensionColumn, error) {
|
||||
dimensionKeys, err := retentionGetter.RuleDimensionKeys(rules)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys := make([]string, 0, len(dimensionKeys)+1)
|
||||
keys = append(keys, zeustypes.MeterDimensionWorkspaceKeyID)
|
||||
for _, key := range dimensionKeys {
|
||||
if key == zeustypes.MeterDimensionWorkspaceKeyID {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, key)
|
||||
}
|
||||
columns := make([]dimensionColumn, len(keys))
|
||||
for i, key := range keys {
|
||||
columns[i] = dimensionColumn{key: key, alias: fmt.Sprintf("dim_%d", i)}
|
||||
}
|
||||
return columns, nil
|
||||
}
|
||||
|
||||
func buildDimensions(
|
||||
orgID valuer.UUID,
|
||||
retentionDays int,
|
||||
retentionRuleIndex int,
|
||||
columns []dimensionColumn,
|
||||
values []string,
|
||||
rules []retentiontypes.CustomRetentionRule,
|
||||
) (map[string]string, error) {
|
||||
if len(columns) != len(values) {
|
||||
return nil, errors.Newf(errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "dimension column/value count mismatch: %d columns, %d values", len(columns), len(values))
|
||||
}
|
||||
|
||||
valuesByKey := make(map[string]string, len(columns))
|
||||
for i, column := range columns {
|
||||
valuesByKey[column.key] = values[i]
|
||||
}
|
||||
|
||||
dimensions := map[string]string{
|
||||
zeustypes.MeterDimensionOrganizationID: orgID.StringValue(),
|
||||
zeustypes.MeterDimensionRetentionDays: strconv.Itoa(retentionDays),
|
||||
}
|
||||
addNonEmpty(dimensions, zeustypes.MeterDimensionWorkspaceKeyID, valuesByKey[zeustypes.MeterDimensionWorkspaceKeyID])
|
||||
|
||||
if retentionRuleIndex < 0 {
|
||||
return dimensions, nil
|
||||
}
|
||||
if retentionRuleIndex >= len(rules) {
|
||||
return nil, errors.Newf(errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "retention rule index %d out of range for %d rules", retentionRuleIndex, len(rules))
|
||||
}
|
||||
for _, filter := range rules[retentionRuleIndex].Filters {
|
||||
addNonEmpty(dimensions, filter.Key, valuesByKey[filter.Key])
|
||||
}
|
||||
return dimensions, nil
|
||||
}
|
||||
|
||||
func addNonEmpty(dimensions map[string]string, key, value string) {
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
dimensions[key] = value
|
||||
}
|
||||
|
||||
func bucketKey(dimensions map[string]string) string {
|
||||
keys := make([]string, 0, len(dimensions))
|
||||
for key := range dimensions {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var b strings.Builder
|
||||
for _, key := range keys {
|
||||
value := dimensions[key]
|
||||
b.WriteString(strconv.Itoa(len(key)))
|
||||
b.WriteByte(':')
|
||||
b.WriteString(key)
|
||||
b.WriteByte('=')
|
||||
b.WriteString(strconv.Itoa(len(value)))
|
||||
b.WriteByte(':')
|
||||
b.WriteString(value)
|
||||
b.WriteByte(';')
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
58
ee/metercollector/logsizemetercollector/provider_test.go
Normal file
58
ee/metercollector/logsizemetercollector/provider_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package logsizemetercollector
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/retentiontypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/zeustypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBuildDimensions(t *testing.T) {
|
||||
orgID := valuer.GenerateUUID()
|
||||
rules := []retentiontypes.CustomRetentionRule{{
|
||||
Filters: []retentiontypes.FilterCondition{{
|
||||
Key: "service.name",
|
||||
Values: []string{"api"},
|
||||
}},
|
||||
TTLDays: 7,
|
||||
}}
|
||||
columns := []dimensionColumn{
|
||||
{key: zeustypes.MeterDimensionWorkspaceKeyID, alias: "dim_0"},
|
||||
{key: "service.name", alias: "dim_1"},
|
||||
}
|
||||
|
||||
dimensions, err := buildDimensions(orgID, 30, 0, columns, []string{"workspace-1", "api"}, rules)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]string{
|
||||
zeustypes.MeterDimensionOrganizationID: orgID.StringValue(),
|
||||
zeustypes.MeterDimensionRetentionDays: "30",
|
||||
zeustypes.MeterDimensionWorkspaceKeyID: "workspace-1",
|
||||
"service.name": "api",
|
||||
}, dimensions)
|
||||
}
|
||||
|
||||
func TestProviderMetadata(t *testing.T) {
|
||||
provider := New(nil, nil)
|
||||
|
||||
require.Equal(t, "signoz.meter.log.size", provider.Name().String())
|
||||
require.Equal(t, zeustypes.MeterUnitBytes, provider.Unit())
|
||||
require.Equal(t, zeustypes.MeterAggregationSum, provider.Aggregation())
|
||||
}
|
||||
|
||||
func TestBucketKeyIsStable(t *testing.T) {
|
||||
first := bucketKey(map[string]string{
|
||||
"service.name": "api",
|
||||
zeustypes.MeterDimensionRetentionDays: "30",
|
||||
zeustypes.MeterDimensionWorkspaceKeyID: "workspace-1",
|
||||
})
|
||||
second := bucketKey(map[string]string{
|
||||
zeustypes.MeterDimensionWorkspaceKeyID: "workspace-1",
|
||||
"service.name": "api",
|
||||
zeustypes.MeterDimensionRetentionDays: "30",
|
||||
})
|
||||
|
||||
require.Equal(t, first, second)
|
||||
require.NotEmpty(t, first)
|
||||
}
|
||||
271
ee/metercollector/spancountmetercollector/provider.go
Normal file
271
ee/metercollector/spancountmetercollector/provider.go
Normal file
@@ -0,0 +1,271 @@
|
||||
// Package spancountmetercollector collects span count meters by workspace and
|
||||
// retention. Keep the query local to this meter.
|
||||
package spancountmetercollector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/metercollector"
|
||||
"github.com/SigNoz/signoz/pkg/modules/retention"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrymeter"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrytraces"
|
||||
"github.com/SigNoz/signoz/pkg/types/retentiontypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/zeustypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// MeterName is the typed registry key for this collector.
|
||||
var (
|
||||
MeterName = zeustypes.MustNewMeterName("signoz.meter.span.count")
|
||||
meterUnit = zeustypes.MeterUnitCount
|
||||
meterAggregation = zeustypes.MeterAggregationSum
|
||||
)
|
||||
|
||||
var _ metercollector.MeterCollector = (*Provider)(nil)
|
||||
|
||||
// Provider collects span count meters.
|
||||
type Provider struct {
|
||||
telemetryStore telemetrystore.TelemetryStore
|
||||
retentionGetter retention.Getter
|
||||
}
|
||||
|
||||
func New(telemetryStore telemetrystore.TelemetryStore, retentionGetter retention.Getter) *Provider {
|
||||
return &Provider{
|
||||
telemetryStore: telemetryStore,
|
||||
retentionGetter: retentionGetter,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) Name() zeustypes.MeterName { return MeterName }
|
||||
func (p *Provider) Unit() zeustypes.MeterUnit { return meterUnit }
|
||||
func (p *Provider) Aggregation() zeustypes.MeterAggregation {
|
||||
return meterAggregation
|
||||
}
|
||||
|
||||
// Collect aggregates span count for the window and emits an empty-day sentinel.
|
||||
func (p *Provider) Collect(ctx context.Context, orgID valuer.UUID, window zeustypes.MeterWindow) ([]zeustypes.Meter, error) {
|
||||
meterName := MeterName.String()
|
||||
|
||||
slices, err := p.retentionGetter.ActiveSlices(
|
||||
ctx,
|
||||
orgID,
|
||||
telemetrytraces.DBName,
|
||||
telemetrytraces.SpanIndexV3LocalTableName,
|
||||
retentiontypes.DefaultTracesRetentionDays,
|
||||
window.StartUnixMilli,
|
||||
window.EndUnixMilli,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "load retention slices for meter %q", meterName)
|
||||
}
|
||||
|
||||
type bucket struct {
|
||||
dimensions map[string]string
|
||||
value float64
|
||||
}
|
||||
accumulator := make(map[string]*bucket)
|
||||
|
||||
for _, slice := range slices {
|
||||
query, args, dimensionColumns, err := buildQuery(meterName, slice, p.retentionGetter)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "build retention query for meter %q", meterName)
|
||||
}
|
||||
|
||||
rows, err := p.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "query meter %q slice [%d, %d)", meterName, slice.StartMs, slice.EndMs)
|
||||
}
|
||||
|
||||
if err := func() error {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
dimensionValues := make([]string, len(dimensionColumns))
|
||||
var retentionDays int32
|
||||
var retentionRuleIndex int32
|
||||
var value float64
|
||||
|
||||
scanDest := make([]any, 0, len(dimensionValues)+3)
|
||||
for i := range dimensionValues {
|
||||
scanDest = append(scanDest, &dimensionValues[i])
|
||||
}
|
||||
scanDest = append(scanDest, &retentionDays, &retentionRuleIndex, &value)
|
||||
|
||||
if err := rows.Scan(scanDest...); err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "scan meter %q slice [%d, %d)", meterName, slice.StartMs, slice.EndMs)
|
||||
}
|
||||
|
||||
dimensions, err := buildDimensions(orgID, int(retentionDays), int(retentionRuleIndex), dimensionColumns, dimensionValues, slice.Rules)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "build dimensions for meter %q slice [%d, %d)", meterName, slice.StartMs, slice.EndMs)
|
||||
}
|
||||
|
||||
key := bucketKey(dimensions)
|
||||
b, ok := accumulator[key]
|
||||
if !ok {
|
||||
b = &bucket{dimensions: dimensions}
|
||||
accumulator[key] = b
|
||||
}
|
||||
b.value += value
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "iterate meter %q slice [%d, %d)", meterName, slice.StartMs, slice.EndMs)
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
meters := make([]zeustypes.Meter, 0, len(accumulator))
|
||||
for _, b := range accumulator {
|
||||
meters = append(meters, zeustypes.NewMeter(MeterName, b.value, meterUnit, meterAggregation, window, b.dimensions))
|
||||
}
|
||||
|
||||
// Empty windows still emit a sentinel so checkpoints can advance.
|
||||
if len(meters) == 0 && len(slices) > 0 {
|
||||
meters = append(meters, zeustypes.NewMeter(MeterName, 0, meterUnit, meterAggregation, window, map[string]string{
|
||||
zeustypes.MeterDimensionOrganizationID: orgID.StringValue(),
|
||||
zeustypes.MeterDimensionRetentionDays: strconv.Itoa(slices[len(slices)-1].DefaultDays),
|
||||
}))
|
||||
}
|
||||
|
||||
return meters, nil
|
||||
}
|
||||
|
||||
// buildQuery stays local because each meter owns its billing query.
|
||||
func buildQuery(meterName string, slice retentiontypes.Slice, retentionGetter retention.Getter) (string, []any, []dimensionColumn, error) {
|
||||
retentionExpr, err := retentionGetter.BuildMultiIfSQL(slice.Rules, slice.DefaultDays)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
retentionRuleIndexExpr, err := retentionGetter.BuildRuleIndexSQL(slice.Rules)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
columns, err := dimensionColumnsFor(retentionGetter, slice.Rules)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
selects := make([]string, 0, len(columns)+3)
|
||||
groupBy := make([]string, 0, len(columns)+2)
|
||||
for _, column := range columns {
|
||||
selects = append(selects, fmt.Sprintf("JSONExtractString(labels, '%s') AS %s", column.key, column.alias))
|
||||
groupBy = append(groupBy, column.alias)
|
||||
}
|
||||
selects = append(selects,
|
||||
retentionExpr+" AS retention_days",
|
||||
retentionRuleIndexExpr+" AS retention_rule_index",
|
||||
"ifNull(sum(value), 0) AS value",
|
||||
)
|
||||
groupBy = append(groupBy, "retention_days", "retention_rule_index")
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
sb.Select(selects...)
|
||||
sb.From(telemetrymeter.DBName + "." + telemetrymeter.SamplesTableName)
|
||||
sb.Where(
|
||||
sb.Equal("metric_name", meterName),
|
||||
sb.GTE("unix_milli", slice.StartMs),
|
||||
sb.LT("unix_milli", slice.EndMs),
|
||||
)
|
||||
sb.GroupBy(groupBy...)
|
||||
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
return query, args, columns, nil
|
||||
}
|
||||
|
||||
type dimensionColumn struct {
|
||||
key string
|
||||
alias string
|
||||
}
|
||||
|
||||
func dimensionColumnsFor(retentionGetter retention.Getter, rules []retentiontypes.CustomRetentionRule) ([]dimensionColumn, error) {
|
||||
dimensionKeys, err := retentionGetter.RuleDimensionKeys(rules)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys := make([]string, 0, len(dimensionKeys)+1)
|
||||
keys = append(keys, zeustypes.MeterDimensionWorkspaceKeyID)
|
||||
for _, key := range dimensionKeys {
|
||||
if key == zeustypes.MeterDimensionWorkspaceKeyID {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, key)
|
||||
}
|
||||
columns := make([]dimensionColumn, len(keys))
|
||||
for i, key := range keys {
|
||||
columns[i] = dimensionColumn{key: key, alias: fmt.Sprintf("dim_%d", i)}
|
||||
}
|
||||
return columns, nil
|
||||
}
|
||||
|
||||
func buildDimensions(
|
||||
orgID valuer.UUID,
|
||||
retentionDays int,
|
||||
retentionRuleIndex int,
|
||||
columns []dimensionColumn,
|
||||
values []string,
|
||||
rules []retentiontypes.CustomRetentionRule,
|
||||
) (map[string]string, error) {
|
||||
if len(columns) != len(values) {
|
||||
return nil, errors.Newf(errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "dimension column/value count mismatch: %d columns, %d values", len(columns), len(values))
|
||||
}
|
||||
|
||||
valuesByKey := make(map[string]string, len(columns))
|
||||
for i, column := range columns {
|
||||
valuesByKey[column.key] = values[i]
|
||||
}
|
||||
|
||||
dimensions := map[string]string{
|
||||
zeustypes.MeterDimensionOrganizationID: orgID.StringValue(),
|
||||
zeustypes.MeterDimensionRetentionDays: strconv.Itoa(retentionDays),
|
||||
}
|
||||
addNonEmpty(dimensions, zeustypes.MeterDimensionWorkspaceKeyID, valuesByKey[zeustypes.MeterDimensionWorkspaceKeyID])
|
||||
|
||||
if retentionRuleIndex < 0 {
|
||||
return dimensions, nil
|
||||
}
|
||||
if retentionRuleIndex >= len(rules) {
|
||||
return nil, errors.Newf(errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "retention rule index %d out of range for %d rules", retentionRuleIndex, len(rules))
|
||||
}
|
||||
for _, filter := range rules[retentionRuleIndex].Filters {
|
||||
addNonEmpty(dimensions, filter.Key, valuesByKey[filter.Key])
|
||||
}
|
||||
return dimensions, nil
|
||||
}
|
||||
|
||||
func addNonEmpty(dimensions map[string]string, key, value string) {
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
dimensions[key] = value
|
||||
}
|
||||
|
||||
func bucketKey(dimensions map[string]string) string {
|
||||
keys := make([]string, 0, len(dimensions))
|
||||
for key := range dimensions {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var b strings.Builder
|
||||
for _, key := range keys {
|
||||
value := dimensions[key]
|
||||
b.WriteString(strconv.Itoa(len(key)))
|
||||
b.WriteByte(':')
|
||||
b.WriteString(key)
|
||||
b.WriteByte('=')
|
||||
b.WriteString(strconv.Itoa(len(value)))
|
||||
b.WriteByte(':')
|
||||
b.WriteString(value)
|
||||
b.WriteByte(';')
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
58
ee/metercollector/spancountmetercollector/provider_test.go
Normal file
58
ee/metercollector/spancountmetercollector/provider_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package spancountmetercollector
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/retentiontypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/zeustypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBuildDimensions(t *testing.T) {
|
||||
orgID := valuer.GenerateUUID()
|
||||
rules := []retentiontypes.CustomRetentionRule{{
|
||||
Filters: []retentiontypes.FilterCondition{{
|
||||
Key: "service.name",
|
||||
Values: []string{"api"},
|
||||
}},
|
||||
TTLDays: 7,
|
||||
}}
|
||||
columns := []dimensionColumn{
|
||||
{key: zeustypes.MeterDimensionWorkspaceKeyID, alias: "dim_0"},
|
||||
{key: "service.name", alias: "dim_1"},
|
||||
}
|
||||
|
||||
dimensions, err := buildDimensions(orgID, 30, 0, columns, []string{"workspace-1", "api"}, rules)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]string{
|
||||
zeustypes.MeterDimensionOrganizationID: orgID.StringValue(),
|
||||
zeustypes.MeterDimensionRetentionDays: "30",
|
||||
zeustypes.MeterDimensionWorkspaceKeyID: "workspace-1",
|
||||
"service.name": "api",
|
||||
}, dimensions)
|
||||
}
|
||||
|
||||
func TestProviderMetadata(t *testing.T) {
|
||||
provider := New(nil, nil)
|
||||
|
||||
require.Equal(t, "signoz.meter.span.count", provider.Name().String())
|
||||
require.Equal(t, zeustypes.MeterUnitCount, provider.Unit())
|
||||
require.Equal(t, zeustypes.MeterAggregationSum, provider.Aggregation())
|
||||
}
|
||||
|
||||
func TestBucketKeyIsStable(t *testing.T) {
|
||||
first := bucketKey(map[string]string{
|
||||
"service.name": "api",
|
||||
zeustypes.MeterDimensionRetentionDays: "30",
|
||||
zeustypes.MeterDimensionWorkspaceKeyID: "workspace-1",
|
||||
})
|
||||
second := bucketKey(map[string]string{
|
||||
zeustypes.MeterDimensionWorkspaceKeyID: "workspace-1",
|
||||
"service.name": "api",
|
||||
zeustypes.MeterDimensionRetentionDays: "30",
|
||||
})
|
||||
|
||||
require.Equal(t, first, second)
|
||||
require.NotEmpty(t, first)
|
||||
}
|
||||
271
ee/metercollector/spansizemetercollector/provider.go
Normal file
271
ee/metercollector/spansizemetercollector/provider.go
Normal file
@@ -0,0 +1,271 @@
|
||||
// Package spansizemetercollector collects span size meters by workspace and
|
||||
// retention. Keep the query local to this meter.
|
||||
package spansizemetercollector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/metercollector"
|
||||
"github.com/SigNoz/signoz/pkg/modules/retention"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrymeter"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrytraces"
|
||||
"github.com/SigNoz/signoz/pkg/types/retentiontypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/zeustypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// MeterName is the typed registry key for this collector.
|
||||
var (
|
||||
MeterName = zeustypes.MustNewMeterName("signoz.meter.span.size")
|
||||
meterUnit = zeustypes.MeterUnitBytes
|
||||
meterAggregation = zeustypes.MeterAggregationSum
|
||||
)
|
||||
|
||||
var _ metercollector.MeterCollector = (*Provider)(nil)
|
||||
|
||||
// Provider collects span size meters.
|
||||
type Provider struct {
|
||||
telemetryStore telemetrystore.TelemetryStore
|
||||
retentionGetter retention.Getter
|
||||
}
|
||||
|
||||
func New(telemetryStore telemetrystore.TelemetryStore, retentionGetter retention.Getter) *Provider {
|
||||
return &Provider{
|
||||
telemetryStore: telemetryStore,
|
||||
retentionGetter: retentionGetter,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) Name() zeustypes.MeterName { return MeterName }
|
||||
func (p *Provider) Unit() zeustypes.MeterUnit { return meterUnit }
|
||||
func (p *Provider) Aggregation() zeustypes.MeterAggregation {
|
||||
return meterAggregation
|
||||
}
|
||||
|
||||
// Collect aggregates span size for the window and emits an empty-day sentinel.
|
||||
func (p *Provider) Collect(ctx context.Context, orgID valuer.UUID, window zeustypes.MeterWindow) ([]zeustypes.Meter, error) {
|
||||
meterName := MeterName.String()
|
||||
|
||||
slices, err := p.retentionGetter.ActiveSlices(
|
||||
ctx,
|
||||
orgID,
|
||||
telemetrytraces.DBName,
|
||||
telemetrytraces.SpanIndexV3LocalTableName,
|
||||
retentiontypes.DefaultTracesRetentionDays,
|
||||
window.StartUnixMilli,
|
||||
window.EndUnixMilli,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "load retention slices for meter %q", meterName)
|
||||
}
|
||||
|
||||
type bucket struct {
|
||||
dimensions map[string]string
|
||||
value float64
|
||||
}
|
||||
accumulator := make(map[string]*bucket)
|
||||
|
||||
for _, slice := range slices {
|
||||
query, args, dimensionColumns, err := buildQuery(meterName, slice, p.retentionGetter)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "build retention query for meter %q", meterName)
|
||||
}
|
||||
|
||||
rows, err := p.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "query meter %q slice [%d, %d)", meterName, slice.StartMs, slice.EndMs)
|
||||
}
|
||||
|
||||
if err := func() error {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
dimensionValues := make([]string, len(dimensionColumns))
|
||||
var retentionDays int32
|
||||
var retentionRuleIndex int32
|
||||
var value float64
|
||||
|
||||
scanDest := make([]any, 0, len(dimensionValues)+3)
|
||||
for i := range dimensionValues {
|
||||
scanDest = append(scanDest, &dimensionValues[i])
|
||||
}
|
||||
scanDest = append(scanDest, &retentionDays, &retentionRuleIndex, &value)
|
||||
|
||||
if err := rows.Scan(scanDest...); err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "scan meter %q slice [%d, %d)", meterName, slice.StartMs, slice.EndMs)
|
||||
}
|
||||
|
||||
dimensions, err := buildDimensions(orgID, int(retentionDays), int(retentionRuleIndex), dimensionColumns, dimensionValues, slice.Rules)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "build dimensions for meter %q slice [%d, %d)", meterName, slice.StartMs, slice.EndMs)
|
||||
}
|
||||
|
||||
key := bucketKey(dimensions)
|
||||
b, ok := accumulator[key]
|
||||
if !ok {
|
||||
b = &bucket{dimensions: dimensions}
|
||||
accumulator[key] = b
|
||||
}
|
||||
b.value += value
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "iterate meter %q slice [%d, %d)", meterName, slice.StartMs, slice.EndMs)
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
meters := make([]zeustypes.Meter, 0, len(accumulator))
|
||||
for _, b := range accumulator {
|
||||
meters = append(meters, zeustypes.NewMeter(MeterName, b.value, meterUnit, meterAggregation, window, b.dimensions))
|
||||
}
|
||||
|
||||
// Empty windows still emit a sentinel so checkpoints can advance.
|
||||
if len(meters) == 0 && len(slices) > 0 {
|
||||
meters = append(meters, zeustypes.NewMeter(MeterName, 0, meterUnit, meterAggregation, window, map[string]string{
|
||||
zeustypes.MeterDimensionOrganizationID: orgID.StringValue(),
|
||||
zeustypes.MeterDimensionRetentionDays: strconv.Itoa(slices[len(slices)-1].DefaultDays),
|
||||
}))
|
||||
}
|
||||
|
||||
return meters, nil
|
||||
}
|
||||
|
||||
// buildQuery stays local because each meter owns its billing query.
|
||||
func buildQuery(meterName string, slice retentiontypes.Slice, retentionGetter retention.Getter) (string, []any, []dimensionColumn, error) {
|
||||
retentionExpr, err := retentionGetter.BuildMultiIfSQL(slice.Rules, slice.DefaultDays)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
retentionRuleIndexExpr, err := retentionGetter.BuildRuleIndexSQL(slice.Rules)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
columns, err := dimensionColumnsFor(retentionGetter, slice.Rules)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
selects := make([]string, 0, len(columns)+3)
|
||||
groupBy := make([]string, 0, len(columns)+2)
|
||||
for _, column := range columns {
|
||||
selects = append(selects, fmt.Sprintf("JSONExtractString(labels, '%s') AS %s", column.key, column.alias))
|
||||
groupBy = append(groupBy, column.alias)
|
||||
}
|
||||
selects = append(selects,
|
||||
retentionExpr+" AS retention_days",
|
||||
retentionRuleIndexExpr+" AS retention_rule_index",
|
||||
"ifNull(sum(value), 0) AS value",
|
||||
)
|
||||
groupBy = append(groupBy, "retention_days", "retention_rule_index")
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
sb.Select(selects...)
|
||||
sb.From(telemetrymeter.DBName + "." + telemetrymeter.SamplesTableName)
|
||||
sb.Where(
|
||||
sb.Equal("metric_name", meterName),
|
||||
sb.GTE("unix_milli", slice.StartMs),
|
||||
sb.LT("unix_milli", slice.EndMs),
|
||||
)
|
||||
sb.GroupBy(groupBy...)
|
||||
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
return query, args, columns, nil
|
||||
}
|
||||
|
||||
type dimensionColumn struct {
|
||||
key string
|
||||
alias string
|
||||
}
|
||||
|
||||
func dimensionColumnsFor(retentionGetter retention.Getter, rules []retentiontypes.CustomRetentionRule) ([]dimensionColumn, error) {
|
||||
dimensionKeys, err := retentionGetter.RuleDimensionKeys(rules)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys := make([]string, 0, len(dimensionKeys)+1)
|
||||
keys = append(keys, zeustypes.MeterDimensionWorkspaceKeyID)
|
||||
for _, key := range dimensionKeys {
|
||||
if key == zeustypes.MeterDimensionWorkspaceKeyID {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, key)
|
||||
}
|
||||
columns := make([]dimensionColumn, len(keys))
|
||||
for i, key := range keys {
|
||||
columns[i] = dimensionColumn{key: key, alias: fmt.Sprintf("dim_%d", i)}
|
||||
}
|
||||
return columns, nil
|
||||
}
|
||||
|
||||
func buildDimensions(
|
||||
orgID valuer.UUID,
|
||||
retentionDays int,
|
||||
retentionRuleIndex int,
|
||||
columns []dimensionColumn,
|
||||
values []string,
|
||||
rules []retentiontypes.CustomRetentionRule,
|
||||
) (map[string]string, error) {
|
||||
if len(columns) != len(values) {
|
||||
return nil, errors.Newf(errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "dimension column/value count mismatch: %d columns, %d values", len(columns), len(values))
|
||||
}
|
||||
|
||||
valuesByKey := make(map[string]string, len(columns))
|
||||
for i, column := range columns {
|
||||
valuesByKey[column.key] = values[i]
|
||||
}
|
||||
|
||||
dimensions := map[string]string{
|
||||
zeustypes.MeterDimensionOrganizationID: orgID.StringValue(),
|
||||
zeustypes.MeterDimensionRetentionDays: strconv.Itoa(retentionDays),
|
||||
}
|
||||
addNonEmpty(dimensions, zeustypes.MeterDimensionWorkspaceKeyID, valuesByKey[zeustypes.MeterDimensionWorkspaceKeyID])
|
||||
|
||||
if retentionRuleIndex < 0 {
|
||||
return dimensions, nil
|
||||
}
|
||||
if retentionRuleIndex >= len(rules) {
|
||||
return nil, errors.Newf(errors.TypeInternal, zeustypes.ErrCodeMeterCollectFailed, "retention rule index %d out of range for %d rules", retentionRuleIndex, len(rules))
|
||||
}
|
||||
for _, filter := range rules[retentionRuleIndex].Filters {
|
||||
addNonEmpty(dimensions, filter.Key, valuesByKey[filter.Key])
|
||||
}
|
||||
return dimensions, nil
|
||||
}
|
||||
|
||||
func addNonEmpty(dimensions map[string]string, key, value string) {
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
dimensions[key] = value
|
||||
}
|
||||
|
||||
func bucketKey(dimensions map[string]string) string {
|
||||
keys := make([]string, 0, len(dimensions))
|
||||
for key := range dimensions {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var b strings.Builder
|
||||
for _, key := range keys {
|
||||
value := dimensions[key]
|
||||
b.WriteString(strconv.Itoa(len(key)))
|
||||
b.WriteByte(':')
|
||||
b.WriteString(key)
|
||||
b.WriteByte('=')
|
||||
b.WriteString(strconv.Itoa(len(value)))
|
||||
b.WriteByte(':')
|
||||
b.WriteString(value)
|
||||
b.WriteByte(';')
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
58
ee/metercollector/spansizemetercollector/provider_test.go
Normal file
58
ee/metercollector/spansizemetercollector/provider_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package spansizemetercollector
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/retentiontypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/zeustypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBuildDimensions(t *testing.T) {
|
||||
orgID := valuer.GenerateUUID()
|
||||
rules := []retentiontypes.CustomRetentionRule{{
|
||||
Filters: []retentiontypes.FilterCondition{{
|
||||
Key: "service.name",
|
||||
Values: []string{"api"},
|
||||
}},
|
||||
TTLDays: 7,
|
||||
}}
|
||||
columns := []dimensionColumn{
|
||||
{key: zeustypes.MeterDimensionWorkspaceKeyID, alias: "dim_0"},
|
||||
{key: "service.name", alias: "dim_1"},
|
||||
}
|
||||
|
||||
dimensions, err := buildDimensions(orgID, 30, 0, columns, []string{"workspace-1", "api"}, rules)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]string{
|
||||
zeustypes.MeterDimensionOrganizationID: orgID.StringValue(),
|
||||
zeustypes.MeterDimensionRetentionDays: "30",
|
||||
zeustypes.MeterDimensionWorkspaceKeyID: "workspace-1",
|
||||
"service.name": "api",
|
||||
}, dimensions)
|
||||
}
|
||||
|
||||
func TestProviderMetadata(t *testing.T) {
|
||||
provider := New(nil, nil)
|
||||
|
||||
require.Equal(t, "signoz.meter.span.size", provider.Name().String())
|
||||
require.Equal(t, zeustypes.MeterUnitBytes, provider.Unit())
|
||||
require.Equal(t, zeustypes.MeterAggregationSum, provider.Aggregation())
|
||||
}
|
||||
|
||||
func TestBucketKeyIsStable(t *testing.T) {
|
||||
first := bucketKey(map[string]string{
|
||||
"service.name": "api",
|
||||
zeustypes.MeterDimensionRetentionDays: "30",
|
||||
zeustypes.MeterDimensionWorkspaceKeyID: "workspace-1",
|
||||
})
|
||||
second := bucketKey(map[string]string{
|
||||
zeustypes.MeterDimensionWorkspaceKeyID: "workspace-1",
|
||||
"service.name": "api",
|
||||
zeustypes.MeterDimensionRetentionDays: "30",
|
||||
})
|
||||
|
||||
require.Equal(t, first, second)
|
||||
require.NotEmpty(t, first)
|
||||
}
|
||||
629
ee/meterreporter/httpmeterreporter/provider.go
Normal file
629
ee/meterreporter/httpmeterreporter/provider.go
Normal file
@@ -0,0 +1,629 @@
|
||||
package httpmeterreporter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/metercollector"
|
||||
"github.com/SigNoz/signoz/pkg/meterreporter"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrymeter"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/types/zeustypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/SigNoz/signoz/pkg/zeus"
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
var _ factory.ServiceWithHealthy = (*Provider)(nil)
|
||||
|
||||
var errCodeReportFailed = errors.MustNewCode("meterreporter_report_failed")
|
||||
|
||||
const (
|
||||
phaseSealed = "sealed"
|
||||
phaseToday = "today"
|
||||
|
||||
attrPhase = "phase"
|
||||
attrResult = "result"
|
||||
attrMeterReporterProvider = "meterreporter.provider"
|
||||
attrOrgID = "meterreporter.org_id"
|
||||
attrOrgCount = "meterreporter.org_count"
|
||||
attrMeter = "meterreporter.meter"
|
||||
attrDate = "meterreporter.date"
|
||||
attrReadings = "meterreporter.readings"
|
||||
attrReadingsCollected = "meterreporter.readings_collected"
|
||||
attrReadingsDropped = "meterreporter.readings_dropped"
|
||||
attrWindowStartUnixMilli = "meterreporter.window_start_unix_milli"
|
||||
attrWindowEndUnixMilli = "meterreporter.window_end_unix_milli"
|
||||
attrWindowCompleted = "meterreporter.window_completed"
|
||||
attrCatchupStart = "meterreporter.catchup_start"
|
||||
attrCatchupEnd = "meterreporter.catchup_end"
|
||||
attrDurationMs = "meterreporter.duration_ms"
|
||||
attrDryRun = "meterreporter.dry_run"
|
||||
attrIdempotencyKey = "meterreporter.idempotency_key"
|
||||
|
||||
resultSuccess = "success"
|
||||
resultFailure = "failure"
|
||||
|
||||
providerName = "http"
|
||||
)
|
||||
|
||||
// Provider collects registered meters and ships them to Zeus.
|
||||
type Provider struct {
|
||||
settings factory.ScopedProviderSettings
|
||||
config meterreporter.Config
|
||||
collectors []metercollector.MeterCollector
|
||||
|
||||
licensing licensing.Licensing
|
||||
telemetryStore telemetrystore.TelemetryStore
|
||||
orgGetter organization.Getter
|
||||
zeus zeus.Zeus
|
||||
|
||||
healthyC chan struct{}
|
||||
stopC chan struct{}
|
||||
goroutinesWg sync.WaitGroup
|
||||
metrics *reporterMetrics
|
||||
}
|
||||
|
||||
// NewFactory registers the HTTP meter reporter.
|
||||
func NewFactory(
|
||||
collectors map[zeustypes.MeterName]metercollector.MeterCollector,
|
||||
licensing licensing.Licensing,
|
||||
telemetryStore telemetrystore.TelemetryStore,
|
||||
orgGetter organization.Getter,
|
||||
zeus zeus.Zeus,
|
||||
) factory.ProviderFactory[meterreporter.Reporter, meterreporter.Config] {
|
||||
return factory.NewProviderFactory(
|
||||
factory.MustNewName(providerName),
|
||||
func(ctx context.Context, providerSettings factory.ProviderSettings, config meterreporter.Config) (meterreporter.Reporter, error) {
|
||||
return newProvider(ctx, providerSettings, config, collectors, licensing, telemetryStore, orgGetter, zeus)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func newProvider(
|
||||
_ context.Context,
|
||||
providerSettings factory.ProviderSettings,
|
||||
config meterreporter.Config,
|
||||
collectors map[zeustypes.MeterName]metercollector.MeterCollector,
|
||||
licensing licensing.Licensing,
|
||||
telemetryStore telemetrystore.TelemetryStore,
|
||||
orgGetter organization.Getter,
|
||||
zeus zeus.Zeus,
|
||||
) (*Provider, error) {
|
||||
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/ee/meterreporter/httpmeterreporter")
|
||||
|
||||
metrics, err := newReporterMetrics(settings.Meter())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orderedCollectors, err := validateCollectors(collectors)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Provider{
|
||||
settings: settings,
|
||||
config: config,
|
||||
collectors: orderedCollectors,
|
||||
licensing: licensing,
|
||||
telemetryStore: telemetryStore,
|
||||
orgGetter: orgGetter,
|
||||
zeus: zeus,
|
||||
healthyC: make(chan struct{}),
|
||||
stopC: make(chan struct{}),
|
||||
metrics: metrics,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func validateCollectors(collectors map[zeustypes.MeterName]metercollector.MeterCollector) ([]metercollector.MeterCollector, error) {
|
||||
ordered := make([]metercollector.MeterCollector, 0, len(collectors))
|
||||
for name, collector := range collectors {
|
||||
if name.IsZero() {
|
||||
return nil, errors.New(errors.TypeInvalidInput, meterreporter.ErrCodeInvalidInput, "empty meter name in collector registry")
|
||||
}
|
||||
if collector == nil {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, meterreporter.ErrCodeInvalidInput, "nil collector for meter %q", name.String())
|
||||
}
|
||||
if collector.Name() != name {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, meterreporter.ErrCodeInvalidInput, "registry key %q does not match collector.Name() %q", name.String(), collector.Name().String())
|
||||
}
|
||||
if collector.Unit().IsZero() {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, meterreporter.ErrCodeInvalidInput, "meter %q has empty unit", name.String())
|
||||
}
|
||||
if collector.Aggregation().IsZero() {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, meterreporter.ErrCodeInvalidInput, "meter %q has empty aggregation", name.String())
|
||||
}
|
||||
ordered = append(ordered, collector)
|
||||
}
|
||||
|
||||
sort.Slice(ordered, func(i, j int) bool {
|
||||
return ordered[i].Name().String() < ordered[j].Name().String()
|
||||
})
|
||||
|
||||
return ordered, nil
|
||||
}
|
||||
|
||||
// Start runs an immediate tick, then repeats on Config.Interval.
|
||||
func (provider *Provider) Start(ctx context.Context) error {
|
||||
close(provider.healthyC)
|
||||
|
||||
provider.settings.Logger().InfoContext(ctx, "meter reporter started",
|
||||
slog.Duration("interval", provider.config.Interval),
|
||||
slog.Duration("timeout", provider.config.Timeout),
|
||||
slog.Int("catchup_max_days_per_tick", provider.config.CatchupMaxDaysPerTick),
|
||||
slog.Int("meters", len(provider.collectors)),
|
||||
)
|
||||
|
||||
provider.goroutinesWg.Add(1)
|
||||
go func() {
|
||||
defer provider.goroutinesWg.Done()
|
||||
|
||||
provider.runTick(ctx)
|
||||
|
||||
ticker := time.NewTicker(provider.config.Interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-provider.stopC:
|
||||
return
|
||||
case <-ticker.C:
|
||||
provider.runTick(ctx)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
provider.goroutinesWg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop signals the tick loop and waits for any in-flight tick.
|
||||
func (provider *Provider) Stop(ctx context.Context) error {
|
||||
<-provider.healthyC
|
||||
provider.settings.Logger().InfoContext(ctx, "meter reporter stopping")
|
||||
select {
|
||||
case <-provider.stopC:
|
||||
// already closed
|
||||
default:
|
||||
close(provider.stopC)
|
||||
}
|
||||
provider.goroutinesWg.Wait()
|
||||
provider.settings.Logger().InfoContext(ctx, "meter reporter stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *Provider) Healthy() <-chan struct{} {
|
||||
return provider.healthyC
|
||||
}
|
||||
|
||||
// runTick executes one collect-and-ship cycle under Config.Timeout.
|
||||
func (provider *Provider) runTick(parentCtx context.Context) {
|
||||
tickStart := time.Now()
|
||||
ctx, span := provider.settings.Tracer().Start(parentCtx, "meterreporter.Tick", trace.WithAttributes(
|
||||
attribute.String(attrMeterReporterProvider, providerName),
|
||||
attribute.Int("meterreporter.meters", len(provider.collectors)),
|
||||
attribute.Int("meterreporter.catchup_max_days_per_tick", provider.config.CatchupMaxDaysPerTick),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
provider.metrics.ticks.Add(ctx, 1)
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, provider.config.Timeout)
|
||||
defer cancel()
|
||||
|
||||
provider.settings.Logger().DebugContext(ctx, "meter reporter tick started",
|
||||
slog.Duration("timeout", provider.config.Timeout),
|
||||
slog.Int("meters", len(provider.collectors)),
|
||||
)
|
||||
|
||||
if err := provider.tick(ctx); err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
span.SetAttributes(
|
||||
attribute.String(attrResult, resultFailure),
|
||||
attribute.Int64(attrDurationMs, time.Since(tickStart).Milliseconds()),
|
||||
)
|
||||
provider.settings.Logger().ErrorContext(ctx, "meter reporter tick failed",
|
||||
errors.Attr(err),
|
||||
slog.Duration("timeout", provider.config.Timeout),
|
||||
slog.Duration("duration", time.Since(tickStart)),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.String(attrResult, resultSuccess),
|
||||
attribute.Int64(attrDurationMs, time.Since(tickStart).Milliseconds()),
|
||||
)
|
||||
provider.settings.Logger().DebugContext(ctx, "meter reporter tick completed", slog.Duration("duration", time.Since(tickStart)))
|
||||
}
|
||||
|
||||
// tick processes sealed catchup days, then today's partial window.
|
||||
func (provider *Provider) tick(ctx context.Context) error {
|
||||
now := time.Now().UTC()
|
||||
// Use one timestamp so a tick cannot straddle midnight.
|
||||
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
|
||||
yesterday := todayStart.AddDate(0, 0, -1)
|
||||
|
||||
orgs, err := provider.orgGetter.ListByOwnedKeyRange(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, errCodeReportFailed, "failed to list organizations")
|
||||
}
|
||||
trace.SpanFromContext(ctx).SetAttributes(attribute.Int(attrOrgCount, len(orgs)))
|
||||
if len(orgs) == 0 {
|
||||
provider.settings.Logger().InfoContext(ctx, "skipping meter reporter tick; no organizations found")
|
||||
return nil
|
||||
}
|
||||
org := orgs[0]
|
||||
if len(orgs) > 1 {
|
||||
// signoz_meter samples have no org marker.
|
||||
provider.settings.Logger().WarnContext(ctx, "multiple orgs on a single instance; reporting only the first",
|
||||
slog.Int("org_count", len(orgs)),
|
||||
slog.String("selected_org_id", org.ID.StringValue()),
|
||||
)
|
||||
}
|
||||
trace.SpanFromContext(ctx).SetAttributes(attribute.String(attrOrgID, org.ID.StringValue()))
|
||||
|
||||
license, err := provider.licensing.GetActive(ctx, org.ID)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, errCodeReportFailed, "failed to fetch active license for org %q", org.ID.StringValue())
|
||||
}
|
||||
if license == nil || license.Key == "" {
|
||||
provider.settings.Logger().WarnContext(ctx, "skipping tick, nil/empty license for org", slog.String("org_id", org.ID.StringValue()))
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: re-enable once /v2/meters/checkpoints is live in staging. Until
|
||||
// then we run with an empty checkpoint map; bootstrap floors are taken
|
||||
// from data and dropCheckpointed becomes a no-op for the sealed window.
|
||||
// checkpoints, err := provider.zeus.GetMeterCheckpoints(ctx, license.Key)
|
||||
// if err != nil {
|
||||
// provider.metrics.checkpointErrors.Add(ctx, 1)
|
||||
// provider.settings.Logger().ErrorContext(ctx, "skipping tick: meter checkpoints call failed", errors.Attr(err))
|
||||
// return nil
|
||||
// }
|
||||
// checkpointsByMeter := make(map[string]time.Time, len(checkpoints))
|
||||
// for _, checkpoint := range checkpoints {
|
||||
// checkpointsByMeter[checkpoint.Name] = checkpoint.Checkpoint.UTC()
|
||||
// }
|
||||
checkpointsByMeter := make(map[string]time.Time)
|
||||
|
||||
floor := provider.dataFloor(ctx, todayStart)
|
||||
catchupStart := provider.catchupStart(floor, todayStart, checkpointsByMeter)
|
||||
end := catchupStart.AddDate(0, 0, provider.config.CatchupMaxDaysPerTick-1)
|
||||
if end.After(yesterday) {
|
||||
end = yesterday
|
||||
}
|
||||
trace.SpanFromContext(ctx).SetAttributes(
|
||||
attribute.String(attrCatchupStart, catchupStart.Format("2006-01-02")),
|
||||
attribute.String(attrCatchupEnd, end.Format("2006-01-02")),
|
||||
)
|
||||
provider.settings.Logger().DebugContext(ctx, "meter reporter catchup window selected",
|
||||
slog.String("org_id", org.ID.StringValue()),
|
||||
slog.Time("data_floor", floor),
|
||||
slog.Time("catchup_start", catchupStart),
|
||||
slog.Time("catchup_end", end),
|
||||
slog.Int("catchup_max_days_per_tick", provider.config.CatchupMaxDaysPerTick),
|
||||
)
|
||||
for day := catchupStart; !day.After(end); day = day.AddDate(0, 0, 1) {
|
||||
window, err := zeustypes.NewMeterWindow(day.UnixMilli(), day.AddDate(0, 0, 1).UnixMilli(), true)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, errCodeReportFailed, "build sealed meter window")
|
||||
}
|
||||
err = provider.runPhase(ctx, org.ID, license.Key, window, checkpointsByMeter)
|
||||
result := resultSuccess
|
||||
if err != nil {
|
||||
result = resultFailure
|
||||
}
|
||||
provider.metrics.catchupDaysProcessed.Add(ctx, 1, metric.WithAttributes(attribute.String(attrResult, result)))
|
||||
if err != nil {
|
||||
provider.settings.Logger().WarnContext(ctx, "stopping sealed catchup after failed day",
|
||||
errors.Attr(err),
|
||||
slog.String("date", day.Format("2006-01-02")),
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Today's partial window runs every tick.
|
||||
if now.UnixMilli() > todayStart.UnixMilli() {
|
||||
todayWindow, err := zeustypes.NewMeterWindow(todayStart.UnixMilli(), now.UnixMilli(), false)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, errCodeReportFailed, "build current-day meter window")
|
||||
}
|
||||
_ = provider.runPhase(ctx, org.ID, license.Key, todayWindow, checkpointsByMeter)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// runPhase collects all meters for one window and ships the batch.
|
||||
func (provider *Provider) runPhase(ctx context.Context, orgID valuer.UUID, licenseKey string, window zeustypes.MeterWindow, checkpointsByMeter map[string]time.Time) error {
|
||||
phaseLabel := phaseToday
|
||||
if window.IsCompleted {
|
||||
phaseLabel = phaseSealed
|
||||
}
|
||||
phaseAttr := metric.WithAttributes(attribute.String(attrPhase, phaseLabel))
|
||||
date := time.UnixMilli(window.StartUnixMilli).UTC().Format("2006-01-02")
|
||||
phaseStart := time.Now()
|
||||
ctx, span := provider.settings.Tracer().Start(ctx, "meterreporter.RunPhase", trace.WithAttributes(
|
||||
attribute.String(attrPhase, phaseLabel),
|
||||
attribute.String(attrOrgID, orgID.StringValue()),
|
||||
attribute.String(attrDate, date),
|
||||
attribute.Int64(attrWindowStartUnixMilli, window.StartUnixMilli),
|
||||
attribute.Int64(attrWindowEndUnixMilli, window.EndUnixMilli),
|
||||
attribute.Bool(attrWindowCompleted, window.IsCompleted),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
provider.settings.Logger().DebugContext(ctx, "meter reporter phase started",
|
||||
slog.String("org_id", orgID.StringValue()),
|
||||
slog.String("phase", phaseLabel),
|
||||
slog.String("date", date),
|
||||
slog.Int64("start_unix_milli", window.StartUnixMilli),
|
||||
slog.Int64("end_unix_milli", window.EndUnixMilli),
|
||||
slog.Int("meters", len(provider.collectors)),
|
||||
)
|
||||
|
||||
collectStart := time.Now()
|
||||
readings := make([]zeustypes.Meter, 0, len(provider.collectors))
|
||||
for _, collector := range provider.collectors {
|
||||
meterName := collector.Name().String()
|
||||
collectStart := time.Now()
|
||||
collectCtx, collectSpan := provider.settings.Tracer().Start(ctx, "meterreporter.CollectMeter", trace.WithAttributes(
|
||||
attribute.String(attrPhase, phaseLabel),
|
||||
attribute.String(attrOrgID, orgID.StringValue()),
|
||||
attribute.String(attrMeter, meterName),
|
||||
attribute.String(attrDate, date),
|
||||
attribute.Int64(attrWindowStartUnixMilli, window.StartUnixMilli),
|
||||
attribute.Int64(attrWindowEndUnixMilli, window.EndUnixMilli),
|
||||
attribute.Bool(attrWindowCompleted, window.IsCompleted),
|
||||
))
|
||||
collectedReadings, err := collector.Collect(collectCtx, orgID, window)
|
||||
if err != nil {
|
||||
collectSpan.RecordError(err)
|
||||
collectSpan.SetStatus(codes.Error, err.Error())
|
||||
collectSpan.SetAttributes(
|
||||
attribute.String(attrResult, resultFailure),
|
||||
attribute.Int64(attrDurationMs, time.Since(collectStart).Milliseconds()),
|
||||
)
|
||||
collectSpan.End()
|
||||
provider.metrics.collectErrors.Add(ctx, 1, phaseAttr)
|
||||
provider.settings.Logger().WarnContext(ctx, "meter collection failed",
|
||||
errors.Attr(err),
|
||||
slog.String("meter", meterName),
|
||||
slog.String("org_id", orgID.StringValue()),
|
||||
slog.String("phase", phaseLabel),
|
||||
slog.String("date", date),
|
||||
slog.Duration("duration", time.Since(collectStart)),
|
||||
)
|
||||
continue
|
||||
}
|
||||
collectSpan.SetAttributes(
|
||||
attribute.String(attrResult, resultSuccess),
|
||||
attribute.Int(attrReadings, len(collectedReadings)),
|
||||
attribute.Int64(attrDurationMs, time.Since(collectStart).Milliseconds()),
|
||||
)
|
||||
collectSpan.End()
|
||||
provider.settings.Logger().DebugContext(ctx, "meter collection completed",
|
||||
slog.String("meter", meterName),
|
||||
slog.String("org_id", orgID.StringValue()),
|
||||
slog.String("phase", phaseLabel),
|
||||
slog.String("date", date),
|
||||
slog.Int("readings", len(collectedReadings)),
|
||||
slog.Duration("duration", time.Since(collectStart)),
|
||||
)
|
||||
readings = append(readings, collectedReadings...)
|
||||
}
|
||||
collectDuration := time.Since(collectStart)
|
||||
provider.metrics.collectDuration.Add(ctx, collectDuration.Seconds(), phaseAttr)
|
||||
provider.metrics.collectOperations.Add(ctx, 1, phaseAttr)
|
||||
span.SetAttributes(attribute.Int(attrReadingsCollected, len(readings)))
|
||||
|
||||
if window.IsCompleted {
|
||||
beforeDrop := len(readings)
|
||||
readings = dropCheckpointed(readings, time.UnixMilli(window.StartUnixMilli).UTC(), checkpointsByMeter)
|
||||
dropped := beforeDrop - len(readings)
|
||||
span.SetAttributes(attribute.Int(attrReadingsDropped, dropped))
|
||||
if dropped > 0 {
|
||||
provider.settings.Logger().DebugContext(ctx, "dropped checkpointed meter readings",
|
||||
slog.String("org_id", orgID.StringValue()),
|
||||
slog.String("phase", phaseLabel),
|
||||
slog.String("date", date),
|
||||
slog.Int("dropped", dropped),
|
||||
slog.Int("remaining", len(readings)),
|
||||
)
|
||||
}
|
||||
}
|
||||
if len(readings) == 0 {
|
||||
span.SetAttributes(
|
||||
attribute.String(attrResult, resultSuccess),
|
||||
attribute.Int(attrReadings, 0),
|
||||
attribute.Int64(attrDurationMs, time.Since(phaseStart).Milliseconds()),
|
||||
)
|
||||
provider.settings.Logger().DebugContext(ctx, "meter reporter phase produced no readings",
|
||||
slog.String("org_id", orgID.StringValue()),
|
||||
slog.String("phase", phaseLabel),
|
||||
slog.String("date", date),
|
||||
slog.Duration("collect_duration", collectDuration),
|
||||
slog.Duration("duration", time.Since(phaseStart)),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
shipStart := time.Now()
|
||||
err := provider.shipReadings(ctx, licenseKey, date, readings)
|
||||
shipDuration := time.Since(shipStart)
|
||||
provider.metrics.shipDuration.Add(ctx, shipDuration.Seconds(), phaseAttr)
|
||||
provider.metrics.shipOperations.Add(ctx, 1, phaseAttr)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
span.SetAttributes(attribute.String(attrResult, resultFailure))
|
||||
provider.metrics.postErrors.Add(ctx, 1, phaseAttr)
|
||||
provider.settings.Logger().ErrorContext(ctx, "failed to ship meter readings",
|
||||
errors.Attr(err),
|
||||
slog.String("phase", phaseLabel),
|
||||
slog.String("date", date),
|
||||
slog.Int("readings", len(readings)),
|
||||
slog.Duration("ship_duration", shipDuration),
|
||||
)
|
||||
return err
|
||||
}
|
||||
provider.metrics.readingsEmitted.Add(ctx, int64(len(readings)), phaseAttr)
|
||||
span.SetAttributes(
|
||||
attribute.String(attrResult, resultSuccess),
|
||||
attribute.Int(attrReadings, len(readings)),
|
||||
attribute.Int64(attrDurationMs, time.Since(phaseStart).Milliseconds()),
|
||||
)
|
||||
provider.settings.Logger().InfoContext(ctx, "meter reporter phase shipped",
|
||||
slog.String("org_id", orgID.StringValue()),
|
||||
slog.String("phase", phaseLabel),
|
||||
slog.String("date", date),
|
||||
slog.Int("readings", len(readings)),
|
||||
slog.Duration("collect_duration", collectDuration),
|
||||
slog.Duration("ship_duration", shipDuration),
|
||||
slog.Duration("duration", time.Since(phaseStart)),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// dropCheckpointed removes readings already covered by meter checkpoints.
|
||||
func dropCheckpointed(readings []zeustypes.Meter, windowDay time.Time, checkpointsByMeter map[string]time.Time) []zeustypes.Meter {
|
||||
if len(checkpointsByMeter) == 0 {
|
||||
return readings
|
||||
}
|
||||
kept := readings[:0]
|
||||
for _, reading := range readings {
|
||||
checkpoint, ok := checkpointsByMeter[reading.MeterName]
|
||||
if !ok || checkpoint.Before(windowDay) {
|
||||
kept = append(kept, reading)
|
||||
}
|
||||
}
|
||||
return kept
|
||||
}
|
||||
|
||||
// catchupStart returns the earliest UTC day that still needs sealed reporting.
|
||||
func (provider *Provider) catchupStart(floor time.Time, todayStart time.Time, checkpointsByMeter map[string]time.Time) time.Time {
|
||||
catchupStart := todayStart
|
||||
|
||||
for _, collector := range provider.collectors {
|
||||
next := floor
|
||||
if checkpoint, ok := checkpointsByMeter[collector.Name().String()]; ok {
|
||||
next = checkpoint.AddDate(0, 0, 1)
|
||||
if next.Before(floor) {
|
||||
next = floor
|
||||
}
|
||||
}
|
||||
if next.Before(catchupStart) {
|
||||
catchupStart = next
|
||||
}
|
||||
}
|
||||
|
||||
yesterday := todayStart.AddDate(0, 0, -1)
|
||||
if catchupStart.After(yesterday) {
|
||||
catchupStart = yesterday
|
||||
}
|
||||
|
||||
return catchupStart
|
||||
}
|
||||
|
||||
// dataFloor returns the earliest signoz_meter sample day, or today on failure.
|
||||
func (provider *Provider) dataFloor(ctx context.Context, todayStart time.Time) time.Time {
|
||||
ctx, span := provider.settings.Tracer().Start(ctx, "meterreporter.DataFloor")
|
||||
defer span.End()
|
||||
|
||||
if provider.telemetryStore == nil {
|
||||
span.SetAttributes(attribute.String(attrResult, resultSuccess))
|
||||
return todayStart
|
||||
}
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
sb.Select("ifNull(min(unix_milli), 0)")
|
||||
sb.From(telemetrymeter.DBName + "." + telemetrymeter.SamplesTableName)
|
||||
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
var minMs int64
|
||||
if err := provider.telemetryStore.ClickhouseDB().QueryRow(ctx, query, args...).Scan(&minMs); err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
span.SetAttributes(attribute.String(attrResult, resultFailure))
|
||||
provider.settings.Logger().WarnContext(ctx, "failed to read data floor; falling back to latest sealed day", errors.Attr(err))
|
||||
return todayStart
|
||||
}
|
||||
if minMs == 0 {
|
||||
span.SetAttributes(
|
||||
attribute.String(attrResult, resultSuccess),
|
||||
attribute.Int64("meterreporter.data_floor_unix_milli", 0),
|
||||
)
|
||||
return todayStart
|
||||
}
|
||||
|
||||
minDay := time.UnixMilli(minMs).UTC()
|
||||
floor := time.Date(minDay.Year(), minDay.Month(), minDay.Day(), 0, 0, 0, 0, time.UTC)
|
||||
span.SetAttributes(
|
||||
attribute.String(attrResult, resultSuccess),
|
||||
attribute.Int64("meterreporter.data_floor_unix_milli", floor.UnixMilli()),
|
||||
)
|
||||
provider.settings.Logger().DebugContext(ctx, "meter reporter data floor loaded", slog.Time("data_floor", floor))
|
||||
return floor
|
||||
}
|
||||
|
||||
// shipReadings sends one day's meter batch to Zeus.
|
||||
func (provider *Provider) shipReadings(ctx context.Context, licenseKey string, date string, readings []zeustypes.Meter) error {
|
||||
idempotencyKey := fmt.Sprintf("meter-cron:%s", date)
|
||||
ctx, span := provider.settings.Tracer().Start(ctx, "meterreporter.ShipReadings", trace.WithAttributes(
|
||||
attribute.String(attrDate, date),
|
||||
attribute.Int(attrReadings, len(readings)),
|
||||
attribute.String(attrIdempotencyKey, idempotencyKey),
|
||||
attribute.Bool(attrDryRun, true),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
provider.settings.Logger().InfoContext(ctx, "meter readings prepared for shipment",
|
||||
slog.String("date", date),
|
||||
slog.Int("readings", len(readings)),
|
||||
slog.String("idempotency_key", idempotencyKey),
|
||||
slog.Bool("dry_run", true),
|
||||
)
|
||||
|
||||
// Temporary visibility while /v2/meters is offline.
|
||||
for _, reading := range readings {
|
||||
provider.settings.Logger().InfoContext(ctx, "meter reading prepared for shipment",
|
||||
slog.String("meter", reading.MeterName),
|
||||
slog.Float64("value", reading.Value),
|
||||
slog.String("unit", reading.Unit.StringValue()),
|
||||
slog.String("aggregation", reading.Aggregation.StringValue()),
|
||||
slog.Int64("start_unix_milli", reading.StartUnixMilli),
|
||||
slog.Int64("end_unix_milli", reading.EndUnixMilli),
|
||||
slog.Bool("is_completed", reading.IsCompleted),
|
||||
slog.Any("dimensions", reading.Dimensions),
|
||||
slog.String("idempotency_key", idempotencyKey),
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: re-enable once /v2/meters is live in staging.
|
||||
// body, err := json.Marshal(zeustypes.PostableMeters{Meters: readings})
|
||||
// if err != nil {
|
||||
// return errors.Wrapf(err, errors.TypeInternal, errCodeReportFailed, "marshal meter readings for %s", date)
|
||||
// }
|
||||
// if err := provider.zeus.PutMetersV3(ctx, licenseKey, idempotencyKey, body); err != nil {
|
||||
// return errors.Wrapf(err, errors.TypeInternal, errCodeReportFailed, "ship meter readings for %s", date)
|
||||
// }
|
||||
_ = licenseKey
|
||||
span.SetAttributes(attribute.String(attrResult, resultSuccess))
|
||||
return nil
|
||||
}
|
||||
103
ee/meterreporter/httpmeterreporter/provider_internal_test.go
Normal file
103
ee/meterreporter/httpmeterreporter/provider_internal_test.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package httpmeterreporter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/metercollector"
|
||||
"github.com/SigNoz/signoz/pkg/types/zeustypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestValidateCollectorsRejectsBadRegistry(t *testing.T) {
|
||||
meterA := zeustypes.MustNewMeterName("signoz.test.a")
|
||||
meterB := zeustypes.MustNewMeterName("signoz.test.b")
|
||||
|
||||
t.Run("key name mismatch", func(t *testing.T) {
|
||||
_, err := validateCollectors(map[zeustypes.MeterName]metercollector.MeterCollector{
|
||||
meterA: testCollector{name: meterB},
|
||||
})
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("nil collector", func(t *testing.T) {
|
||||
_, err := validateCollectors(map[zeustypes.MeterName]metercollector.MeterCollector{
|
||||
meterA: nil,
|
||||
})
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDropCheckpointed(t *testing.T) {
|
||||
meterA := zeustypes.MustNewMeterName("signoz.test.a")
|
||||
meterB := zeustypes.MustNewMeterName("signoz.test.b")
|
||||
meterC := zeustypes.MustNewMeterName("signoz.test.c")
|
||||
windowDay := time.Date(2026, 5, 4, 0, 0, 0, 0, time.UTC)
|
||||
window := zeustypes.MustNewMeterWindow(windowDay.UnixMilli(), windowDay.AddDate(0, 0, 1).UnixMilli(), true)
|
||||
readings := []zeustypes.Meter{
|
||||
zeustypes.NewMeter(meterA, 0, zeustypes.MeterUnitCount, zeustypes.MeterAggregationSum, window, nil),
|
||||
zeustypes.NewMeter(meterB, 0, zeustypes.MeterUnitCount, zeustypes.MeterAggregationSum, window, nil),
|
||||
zeustypes.NewMeter(meterC, 0, zeustypes.MeterUnitCount, zeustypes.MeterAggregationSum, window, nil),
|
||||
}
|
||||
|
||||
kept := dropCheckpointed(readings, windowDay, map[string]time.Time{
|
||||
meterA.String(): windowDay,
|
||||
meterB.String(): windowDay.AddDate(0, 0, -1),
|
||||
})
|
||||
|
||||
require.Equal(t, []zeustypes.Meter{
|
||||
zeustypes.NewMeter(meterB, 0, zeustypes.MeterUnitCount, zeustypes.MeterAggregationSum, window, nil),
|
||||
zeustypes.NewMeter(meterC, 0, zeustypes.MeterUnitCount, zeustypes.MeterAggregationSum, window, nil),
|
||||
}, kept)
|
||||
}
|
||||
|
||||
func TestCatchupStart(t *testing.T) {
|
||||
meterA := zeustypes.MustNewMeterName("signoz.test.a")
|
||||
floor := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||
todayStart := time.Date(2026, 5, 5, 0, 0, 0, 0, time.UTC)
|
||||
provider := &Provider{
|
||||
collectors: []metercollector.MeterCollector{
|
||||
testCollector{name: meterA},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("no checkpoint starts at floor", func(t *testing.T) {
|
||||
require.Equal(t, floor, provider.catchupStart(floor, todayStart, nil))
|
||||
})
|
||||
|
||||
t.Run("checkpoint advances by one day", func(t *testing.T) {
|
||||
require.Equal(t, floor.AddDate(0, 0, 2), provider.catchupStart(floor, todayStart, map[string]time.Time{
|
||||
meterA.String(): floor.AddDate(0, 0, 1),
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
type testCollector struct {
|
||||
name zeustypes.MeterName
|
||||
unit zeustypes.MeterUnit
|
||||
aggregation zeustypes.MeterAggregation
|
||||
}
|
||||
|
||||
func (c testCollector) Name() zeustypes.MeterName {
|
||||
return c.name
|
||||
}
|
||||
|
||||
func (c testCollector) Unit() zeustypes.MeterUnit {
|
||||
if c.unit.IsZero() {
|
||||
return zeustypes.MeterUnitCount
|
||||
}
|
||||
return c.unit
|
||||
}
|
||||
|
||||
func (c testCollector) Aggregation() zeustypes.MeterAggregation {
|
||||
if c.aggregation.IsZero() {
|
||||
return zeustypes.MeterAggregationSum
|
||||
}
|
||||
return c.aggregation
|
||||
}
|
||||
|
||||
func (c testCollector) Collect(context.Context, valuer.UUID, zeustypes.MeterWindow) ([]zeustypes.Meter, error) {
|
||||
return nil, nil
|
||||
}
|
||||
90
ee/meterreporter/httpmeterreporter/telemetry.go
Normal file
90
ee/meterreporter/httpmeterreporter/telemetry.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package httpmeterreporter
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
)
|
||||
|
||||
type reporterMetrics struct {
|
||||
ticks metric.Int64Counter
|
||||
readingsEmitted metric.Int64Counter
|
||||
collectErrors metric.Int64Counter
|
||||
postErrors metric.Int64Counter
|
||||
checkpointErrors metric.Int64Counter
|
||||
catchupDaysProcessed metric.Int64Counter
|
||||
collectDuration metric.Float64Counter
|
||||
collectOperations metric.Int64Counter
|
||||
shipDuration metric.Float64Counter
|
||||
shipOperations metric.Int64Counter
|
||||
}
|
||||
|
||||
func newReporterMetrics(meter metric.Meter) (*reporterMetrics, error) {
|
||||
var errs error
|
||||
|
||||
ticks, err := meter.Int64Counter("signoz.meterreporter.ticks", metric.WithDescription("Meter reporter ticks."))
|
||||
if err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
|
||||
readingsEmitted, err := meter.Int64Counter("signoz.meterreporter.readings.emitted", metric.WithDescription("Meter readings shipped to Zeus."))
|
||||
if err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
|
||||
collectErrors, err := meter.Int64Counter("signoz.meterreporter.collect.errors", metric.WithDescription("Meter collection errors."))
|
||||
if err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
|
||||
postErrors, err := meter.Int64Counter("signoz.meterreporter.post.errors", metric.WithDescription("Zeus POST failures."))
|
||||
if err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
|
||||
checkpointErrors, err := meter.Int64Counter("signoz.meterreporter.checkpoint.errors", metric.WithDescription("Zeus checkpoint read failures."))
|
||||
if err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
|
||||
catchupDaysProcessed, err := meter.Int64Counter("signoz.meterreporter.catchup.days_processed", metric.WithDescription("Sealed catchup days processed."))
|
||||
if err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
|
||||
collectDuration, err := meter.Float64Counter("signoz.meterreporter.collect.duration.seconds", metric.WithDescription("Cumulative collection duration."), metric.WithUnit("s"))
|
||||
if err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
|
||||
collectOperations, err := meter.Int64Counter("signoz.meterreporter.collect.operations", metric.WithDescription("Collection phases measured."))
|
||||
if err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
|
||||
shipDuration, err := meter.Float64Counter("signoz.meterreporter.ship.duration.seconds", metric.WithDescription("Cumulative ship duration."), metric.WithUnit("s"))
|
||||
if err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
|
||||
shipOperations, err := meter.Int64Counter("signoz.meterreporter.ship.operations", metric.WithDescription("Ship phases measured."))
|
||||
if err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
|
||||
if errs != nil {
|
||||
return nil, errs
|
||||
}
|
||||
|
||||
return &reporterMetrics{
|
||||
ticks: ticks,
|
||||
readingsEmitted: readingsEmitted,
|
||||
collectErrors: collectErrors,
|
||||
postErrors: postErrors,
|
||||
checkpointErrors: checkpointErrors,
|
||||
catchupDaysProcessed: catchupDaysProcessed,
|
||||
collectDuration: collectDuration,
|
||||
collectOperations: collectOperations,
|
||||
shipDuration: shipDuration,
|
||||
shipOperations: shipOperations,
|
||||
}, nil
|
||||
}
|
||||
@@ -150,6 +150,72 @@ func (provider *Provider) PutMetersV2(ctx context.Context, key string, data []by
|
||||
return err
|
||||
}
|
||||
|
||||
func (provider *Provider) PutMetersV3(ctx context.Context, key string, idempotencyKey string, data []byte) error {
|
||||
headers := http.Header{}
|
||||
if idempotencyKey != "" {
|
||||
headers.Set("X-Idempotency-Key", idempotencyKey)
|
||||
}
|
||||
|
||||
_, err := provider.doWithHeaders(
|
||||
ctx,
|
||||
provider.config.URL.JoinPath("/v2/meters"),
|
||||
http.MethodPost,
|
||||
key,
|
||||
data,
|
||||
headers,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (provider *Provider) GetMeterCheckpoints(ctx context.Context, key string) ([]zeustypes.MeterCheckpoint, error) {
|
||||
response, err := provider.do(
|
||||
ctx,
|
||||
provider.config.URL.JoinPath("/v2/meters/checkpoints"),
|
||||
http.MethodGet,
|
||||
key,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
checkpointValues := gjson.GetBytes(response, "data.checkpoints")
|
||||
if !checkpointValues.Exists() || checkpointValues.Type == gjson.Null {
|
||||
return nil, errors.Newf(errors.TypeInternal, zeus.ErrCodeResponseMalformed, "meter checkpoints are required")
|
||||
}
|
||||
|
||||
if !checkpointValues.IsArray() {
|
||||
return nil, errors.Newf(errors.TypeInternal, zeus.ErrCodeResponseMalformed, "meter checkpoints must be an array")
|
||||
}
|
||||
|
||||
checkpointResults := checkpointValues.Array()
|
||||
checkpoints := make([]zeustypes.MeterCheckpoint, 0, len(checkpointResults))
|
||||
for _, checkpointValue := range checkpointResults {
|
||||
name := checkpointValue.Get("name").String()
|
||||
if name == "" {
|
||||
return nil, errors.Newf(errors.TypeInternal, zeus.ErrCodeResponseMalformed, "meter checkpoint name is required")
|
||||
}
|
||||
|
||||
checkpointString := checkpointValue.Get("checkpoint").String()
|
||||
if checkpointString == "" {
|
||||
return nil, errors.Newf(errors.TypeInternal, zeus.ErrCodeResponseMalformed, "meter checkpoint is required for %q", name)
|
||||
}
|
||||
|
||||
checkpoint, err := time.Parse("2006-01-02", checkpointString)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, zeus.ErrCodeResponseMalformed, "parse meter checkpoint %q for %q", checkpointString, name)
|
||||
}
|
||||
|
||||
checkpoints = append(checkpoints, zeustypes.MeterCheckpoint{
|
||||
Name: name,
|
||||
Checkpoint: checkpoint,
|
||||
})
|
||||
}
|
||||
|
||||
return checkpoints, nil
|
||||
}
|
||||
|
||||
func (provider *Provider) PutProfile(ctx context.Context, key string, profile *zeustypes.PostableProfile) error {
|
||||
body, err := json.Marshal(profile)
|
||||
if err != nil {
|
||||
@@ -185,12 +251,21 @@ func (provider *Provider) PutHost(ctx context.Context, key string, host *zeustyp
|
||||
}
|
||||
|
||||
func (provider *Provider) do(ctx context.Context, url *url.URL, method string, key string, requestBody []byte) ([]byte, error) {
|
||||
return provider.doWithHeaders(ctx, url, method, key, requestBody, nil)
|
||||
}
|
||||
|
||||
func (provider *Provider) doWithHeaders(ctx context.Context, url *url.URL, method string, key string, requestBody []byte, extraHeaders http.Header) ([]byte, error) {
|
||||
request, err := http.NewRequestWithContext(ctx, method, url.String(), bytes.NewBuffer(requestBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request.Header.Set("X-Signoz-Cloud-Api-Key", key)
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
for k, vs := range extraHeaders {
|
||||
for _, v := range vs {
|
||||
request.Header.Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
response, err := provider.httpClient.Do(request)
|
||||
if err != nil {
|
||||
|
||||
@@ -289,8 +289,6 @@
|
||||
// Prevents navigator.clipboard - use useCopyToClipboard hook instead (disabled in tests via override)
|
||||
"signoz/no-raw-absolute-path": "error",
|
||||
// Prevents window.open(path), window.location.origin + path, window.location.href = path
|
||||
"signoz/no-antd-components": "error",
|
||||
// Prevents the usage of specific antd components in favor of our lib
|
||||
"no-restricted-globals": [
|
||||
"error",
|
||||
{
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
"@signozhq/design-tokens": "2.1.4",
|
||||
"@signozhq/icons": "0.1.0",
|
||||
"@signozhq/resizable": "0.0.2",
|
||||
"@signozhq/ui": "0.0.14",
|
||||
"@signozhq/ui": "0.0.12",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@tanstack/react-virtual": "3.13.22",
|
||||
"@uiw/codemirror-theme-copilot": "4.23.11",
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
/**
|
||||
* Rule: no-antd-components
|
||||
*
|
||||
* Prevents importing specific components from antd.
|
||||
*
|
||||
* This rule catches patterns like:
|
||||
* import { Typography } from 'antd'
|
||||
* import { Typography, Button } from 'antd'
|
||||
* import Typography from 'antd/es/typography'
|
||||
* import { Text } from 'antd/es/typography'
|
||||
*
|
||||
* Add components to BANNED_COMPONENTS to ban them.
|
||||
* Key should be PascalCase component name, will match lowercase path too.
|
||||
*/
|
||||
|
||||
const BANNED_COMPONENTS = {
|
||||
Typography: 'Use @signozhq/ui Typography instead of antd Typography.',
|
||||
};
|
||||
|
||||
export default {
|
||||
create(context) {
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
const source = node.source.value;
|
||||
|
||||
// Check direct antd import: import { Typography } from 'antd'
|
||||
if (source === 'antd') {
|
||||
for (const specifier of node.specifiers) {
|
||||
if (specifier.type !== 'ImportSpecifier') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const importedName = specifier.imported.name;
|
||||
const message = BANNED_COMPONENTS[importedName];
|
||||
|
||||
if (message) {
|
||||
context.report({
|
||||
node: specifier,
|
||||
message: `Do not import '${importedName}' from antd. ${message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check antd/es/<component> import: import Typography from 'antd/es/typography'
|
||||
const match = source.match(/^antd\/es\/([^/]+)/);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pathComponent = match[1].toLowerCase();
|
||||
|
||||
for (const [componentName, message] of Object.entries(BANNED_COMPONENTS)) {
|
||||
if (pathComponent === componentName.toLowerCase()) {
|
||||
context.report({
|
||||
node,
|
||||
message: `Do not import from '${source}'. ${message}`,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -9,7 +9,6 @@ import noZustandGetStateInHooks from './rules/no-zustand-getstate-in-hooks.mjs';
|
||||
import noNavigatorClipboard from './rules/no-navigator-clipboard.mjs';
|
||||
import noUnsupportedAssetPattern from './rules/no-unsupported-asset-pattern.mjs';
|
||||
import noRawAbsolutePath from './rules/no-raw-absolute-path.mjs';
|
||||
import noAntdComponents from './rules/no-antd-components.mjs';
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
@@ -20,6 +19,5 @@ export default {
|
||||
'no-navigator-clipboard': noNavigatorClipboard,
|
||||
'no-unsupported-asset-pattern': noUnsupportedAssetPattern,
|
||||
'no-raw-absolute-path': noRawAbsolutePath,
|
||||
'no-antd-components': noAntdComponents,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Typography } from 'antd';
|
||||
import get from 'api/browser/localstorage/get';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { THEME_MODE } from 'hooks/useDarkMode/constant';
|
||||
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
TableColumnsType,
|
||||
TableColumnType,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import type { FilterDropdownProps } from 'antd/lib/table/interface';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { Select, Spin } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Select, Spin, Typography } from 'antd';
|
||||
import { SelectMaxTagPlaceholder } from 'components/MessagingQueues/MQCommon/MQCommon';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||
import { Divider, Drawer } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Divider, Drawer, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Card } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Card, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { CardContainer } from 'container/GridCardLayout/styles';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Button, Modal } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Button, Modal, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import updateCreditCardApi from 'api/v1/checkout/create';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
|
||||
@@ -554,11 +554,10 @@ function ClientSideQBSearch(
|
||||
>
|
||||
<Tooltip title={chipValue}>
|
||||
<TypographyText
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
ellipsis
|
||||
$isInNin={isInNin}
|
||||
disabled={isDisabled}
|
||||
$isEnabled={!!searchValue}
|
||||
$disabled={isDisabled}
|
||||
onClick={(): void => {
|
||||
if (!isDisabled) {
|
||||
tagEditHandler(value);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { Button, Popover, Radio, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Button, Popover, Radio, Tooltip, Typography } from 'antd';
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import { useExportRawData } from 'hooks/useDownloadOptionsMenu/useDownloadOptionsMenu';
|
||||
import { Download, DownloadIcon, Loader2 } from 'lucide-react';
|
||||
|
||||
@@ -15,8 +15,8 @@ import {
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import axios from 'axios';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { MouseEvent, useCallback } from 'react';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import { Col, Row, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Col, Row, Tooltip, Typography } from 'antd';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useDeleteView } from 'hooks/saveViews/useDeleteView';
|
||||
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||
@@ -82,7 +81,7 @@ function MenuItemGenerator({
|
||||
</Tooltip>
|
||||
</Row>
|
||||
<Row>
|
||||
<Typography.Text color="muted">Created by {createdBy}</Typography.Text>
|
||||
<Typography.Text type="secondary">Created by {createdBy}</Typography.Text>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, Form, Input } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Card, Form, Input, Typography } from 'antd';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useSaveView } from 'hooks/saveViews/useSaveView';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Typography } from 'antd';
|
||||
|
||||
function AnnouncementsModal(): JSX.Element {
|
||||
return (
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { toast } from '@signozhq/ui';
|
||||
import { Button, Input, Radio, RadioChangeEvent } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Button, Input, Radio, RadioChangeEvent, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { handleContactSupport } from 'container/Integrations/utils';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
@@ -136,14 +135,12 @@ function FeedbackModal({ onClose }: { onClose: () => void }): JSX.Element {
|
||||
<div className="feedback-modal-content-footer-info-text">
|
||||
<Typography.Text>
|
||||
Have a specific issue?{' '}
|
||||
<a
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
<Typography.Link
|
||||
className="contact-support-link"
|
||||
onClick={handleContactSupportClick}
|
||||
>
|
||||
Contact Support{' '}
|
||||
</a>
|
||||
</Typography.Link>
|
||||
or{' '}
|
||||
<a
|
||||
href="https://signoz.io/docs/introduction/"
|
||||
|
||||
@@ -4,8 +4,7 @@ import { useSelector } from 'react-redux';
|
||||
import { matchPath, useLocation } from 'react-router-dom';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Switch } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Button, Switch, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Button, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { CheckCircle2, HandPlatter } from 'lucide-react';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { Button, Input } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Button, Input, Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Button, Modal, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Button, Modal, Tooltip, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import updateCreditCardApi from 'api/v1/checkout/create';
|
||||
import cx from 'classnames';
|
||||
|
||||
@@ -4,8 +4,7 @@ import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
|
||||
import { useCopyToClipboard, useLocation } from 'react-use';
|
||||
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||
import { Button } from '@signozhq/ui';
|
||||
import { Divider, Drawer, Radio, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
|
||||
import type { RadioChangeEvent } from 'antd/lib';
|
||||
import cx from 'classnames';
|
||||
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
|
||||
@@ -588,7 +587,7 @@ function LogDetailInner({
|
||||
<div className="log-detail-drawer__footer-hint">
|
||||
<div className="log-detail-drawer__footer-hint-content">
|
||||
<Typography.Text
|
||||
color="muted"
|
||||
type="secondary"
|
||||
className="log-detail-drawer__footer-hint-text"
|
||||
>
|
||||
Use
|
||||
@@ -597,7 +596,7 @@ function LogDetailInner({
|
||||
<span>/</span>
|
||||
<ArrowDown size={14} className="log-detail-drawer__footer-hint-icon" />
|
||||
<Typography.Text
|
||||
color="muted"
|
||||
type="secondary"
|
||||
className="log-detail-drawer__footer-hint-text"
|
||||
>
|
||||
to view previous/next log
|
||||
|
||||
@@ -6,7 +6,7 @@ interface ICategoryHeadingProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
function CategoryHeading({ children }: ICategoryHeadingProps): JSX.Element {
|
||||
return <CategoryHeadingText color="muted">{children}</CategoryHeadingText>;
|
||||
return <CategoryHeadingText type="secondary">{children}</CategoryHeadingText>;
|
||||
}
|
||||
|
||||
export default CategoryHeading;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Typography } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const CategoryHeadingText = styled(Typography.Text)`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { blue } from '@ant-design/colors';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
@@ -89,7 +89,7 @@ function LogSelectedField({
|
||||
</span>
|
||||
</Typography.Text>
|
||||
</AddToQueryHOC>
|
||||
<Typography.Text truncate={1} className={cx('selected-log-kv', fontSize)}>
|
||||
<Typography.Text ellipsis className={cx('selected-log-kv', fontSize)}>
|
||||
<span className={cx('selected-log-field-key', fontSize)}>{': '}</span>
|
||||
<span className={cx('selected-log-value', fontSize)}>
|
||||
{fieldValue || "''"}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Button, Input, InputNumber, Popover, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Button, Input, InputNumber, Popover, Tooltip, Typography } from 'antd';
|
||||
import type { DefaultOptionType } from 'antd/es/select';
|
||||
import cx from 'classnames';
|
||||
import { LogViewMode } from 'container/LogsTable';
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { CaretDownOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||
import { Modal, Select, Spin, Tooltip, Tree, TreeDataNode } from 'antd';
|
||||
import {
|
||||
Modal,
|
||||
Select,
|
||||
Spin,
|
||||
Tooltip,
|
||||
Tree,
|
||||
TreeDataNode,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { OnboardingStatusResponse } from 'api/messagingQueues/onboarding/getOnboardingStatus';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
@@ -77,11 +84,9 @@ function ErrorTitleAndKey({
|
||||
key: `${title}-key-${uuid()}`,
|
||||
title: (
|
||||
<div className="attribute-error-title">
|
||||
<Tooltip title={title}>
|
||||
<Typography.Text className="tree-text" truncate={1}>
|
||||
{title}
|
||||
</Typography.Text>
|
||||
</Tooltip>
|
||||
<Typography.Text className="tree-text" ellipsis={{ tooltip: title }}>
|
||||
{title}
|
||||
</Typography.Text>
|
||||
<Tooltip title={errorMsg}>
|
||||
<div
|
||||
className="attribute-error-warning"
|
||||
@@ -120,11 +125,9 @@ function treeTitleAndKey({
|
||||
key: `${title}-key-${uuid()}`,
|
||||
title: (
|
||||
<div className="attribute-success-title">
|
||||
<Tooltip title={title}>
|
||||
<Typography.Text className="tree-text" truncate={1}>
|
||||
{title}
|
||||
</Typography.Text>
|
||||
</Tooltip>
|
||||
<Typography.Text className="tree-text" ellipsis={{ tooltip: title }}>
|
||||
{title}
|
||||
</Typography.Text>
|
||||
{isLeaf && (
|
||||
<div className="success-attribute-icon">
|
||||
<Tooltip title="Success">
|
||||
|
||||
@@ -13,8 +13,7 @@ import {
|
||||
ReloadOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Checkbox, Select } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Button, Checkbox, Select, Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import TextToolTip from 'components/TextToolTip/TextToolTip';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
@@ -756,7 +755,15 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
}}
|
||||
>
|
||||
<div className="option-content">
|
||||
<Typography.Text truncate={1} className="option-label-text">
|
||||
<Typography.Text
|
||||
ellipsis={{
|
||||
tooltip: {
|
||||
placement: 'right',
|
||||
autoAdjustOverflow: true,
|
||||
},
|
||||
}}
|
||||
className="option-label-text"
|
||||
>
|
||||
{highlightMatchedText(String(option.label || ''), searchText)}
|
||||
</Typography.Text>
|
||||
{(option.type === 'custom' || option.type === 'regex') && (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Button, Tooltip, Typography } from 'antd';
|
||||
import WarningPopover from 'components/WarningPopover/WarningPopover';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useCallback } from 'react';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Button, Tooltip, Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Typography } from 'antd';
|
||||
import eyesEmojiUrl from 'assets/Images/eyesEmoji.svg';
|
||||
|
||||
import styles from './QueryCancelledPlaceholder.module.scss';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
import { Fragment, useMemo, useState } from 'react';
|
||||
import { Button, Checkbox, Input, Skeleton } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Button, Checkbox, Input, Skeleton, Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { removeKeysFromExpression } from 'components/QueryBuilderV2/utils';
|
||||
import {
|
||||
@@ -575,9 +574,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
||||
</section>
|
||||
<section className="right-action">
|
||||
{isOpen && !!attributeValues.length && (
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
<Typography.Text
|
||||
className="clear-all"
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
@@ -586,7 +583,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
||||
}}
|
||||
>
|
||||
Clear All
|
||||
</span>
|
||||
</Typography.Text>
|
||||
)}
|
||||
</section>
|
||||
</section>
|
||||
@@ -643,7 +640,16 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
||||
{filter.customRendererForValue ? (
|
||||
filter.customRendererForValue(value)
|
||||
) : (
|
||||
<Typography.Text className="value-string" truncate={1}>
|
||||
<Typography.Text
|
||||
className="value-string"
|
||||
ellipsis={{
|
||||
tooltip: {
|
||||
placement: 'top',
|
||||
mouseEnterDelay: 0.2,
|
||||
mouseLeaveDelay: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{String(value)}
|
||||
</Typography.Text>
|
||||
)}
|
||||
@@ -671,14 +677,12 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
||||
)}
|
||||
{visibleItemsCount < attributeValues?.length && (
|
||||
<section className="show-more">
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
<Typography.Text
|
||||
className="show-more-text"
|
||||
onClick={(): void => setVisibleItemsCount((prev) => prev + 10)}
|
||||
>
|
||||
Show More...
|
||||
</span>
|
||||
</Typography.Text>
|
||||
</section>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -12,8 +12,7 @@ import {
|
||||
ComboboxList,
|
||||
ComboboxTrigger,
|
||||
} from '@signozhq/ui';
|
||||
import { Skeleton, Switch, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Skeleton, Switch, Tooltip, Typography } from 'antd';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Typography } from 'antd';
|
||||
|
||||
import Time from './Time';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Typography } from 'antd';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Popover } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Popover, Typography } from 'antd';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
@@ -11,12 +11,10 @@ import {
|
||||
Space,
|
||||
SpaceProps,
|
||||
TabsProps,
|
||||
} from 'antd';
|
||||
import {
|
||||
Typography,
|
||||
TypographyTextProps,
|
||||
TypographyTitleProps,
|
||||
} from '@signozhq/ui';
|
||||
} from 'antd';
|
||||
import type { TextProps } from 'antd/lib/typography/Text';
|
||||
import type { TitleProps } from 'antd/lib/typography/Title';
|
||||
import styled, { FlattenSimpleInterpolation } from 'styled-components';
|
||||
|
||||
import { IStyledClass } from './types';
|
||||
@@ -55,13 +53,13 @@ const StyledButton = styled(Button)<TStyledButton>`
|
||||
`;
|
||||
|
||||
const { Text } = Typography;
|
||||
type TStyledTypographyText = TypographyTextProps & IStyledClass;
|
||||
type TStyledTypographyText = TextProps & IStyledClass;
|
||||
const StyledTypographyText = styled(Text)<TStyledTypographyText>`
|
||||
${styledClass}
|
||||
`;
|
||||
|
||||
const { Title } = Typography;
|
||||
type TStyledTypographyTitle = TypographyTitleProps & IStyledClass;
|
||||
type TStyledTypographyTitle = TitleProps & IStyledClass;
|
||||
const StyledTypographyTitle = styled(Title)<TStyledTypographyTitle>`
|
||||
${styledClass}
|
||||
`;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Typography } from 'antd';
|
||||
import { timeItems } from 'container/NewWidget/RightContainer/timeItems';
|
||||
|
||||
export const menuItems = timeItems.map((item) => ({
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Dispatch, SetStateAction, useCallback, useMemo } from 'react';
|
||||
import { DownOutlined } from '@ant-design/icons';
|
||||
import { Button, Dropdown } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Button, Dropdown, Typography } from 'antd';
|
||||
import TimeItems, {
|
||||
timePreferance,
|
||||
timePreferenceType,
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
useRef,
|
||||
} from 'react';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Typography } from 'antd';
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import { LineChart } from 'lucide-react';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ExclamationCircleFilled } from '@ant-design/icons';
|
||||
import { Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Tooltip, Typography } from 'antd';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
|
||||
import { getBackgroundColorAndThresholdCheck } from './utils';
|
||||
|
||||
@@ -2,8 +2,7 @@ import { useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Tooltip, Typography } from 'antd';
|
||||
import getAll from 'api/channels/getAll';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import Spinner from 'components/Spinner';
|
||||
@@ -22,7 +21,7 @@ import { Button, ButtonContainer, RightActionContainer } from './styles';
|
||||
|
||||
import './AllAlertChannels.styles.scss';
|
||||
|
||||
const { Text } = Typography;
|
||||
const { Paragraph } = Typography;
|
||||
|
||||
function AlertChannels(): JSX.Element {
|
||||
const { t } = useTranslation(['channels']);
|
||||
@@ -61,9 +60,9 @@ function AlertChannels(): JSX.Element {
|
||||
return (
|
||||
<div className="alert-channels-container">
|
||||
<ButtonContainer>
|
||||
<Text truncate={1} color="muted">
|
||||
<Paragraph ellipsis type="secondary">
|
||||
{t('sending_channels_note')}
|
||||
</Text>
|
||||
</Paragraph>
|
||||
|
||||
<RightActionContainer>
|
||||
<TextToolTip
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQueries } from 'react-query';
|
||||
@@ -6,7 +5,15 @@ import { useQueries } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Input, Space, TableProps, Tooltip } from 'antd';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Input,
|
||||
Space,
|
||||
TableProps,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import type { ColumnType, TablePaginationConfig } from 'antd/es/table';
|
||||
import type { FilterValue, SorterResult } from 'antd/es/table/interface';
|
||||
import type { ColumnsType } from 'antd/lib/table';
|
||||
@@ -353,7 +360,13 @@ function AllErrors(): JSX.Element {
|
||||
width: 100,
|
||||
render: (value): JSX.Element => (
|
||||
<Tooltip overlay={(): JSX.Element => value}>
|
||||
<Typography.Text truncate={2}>{value}</Typography.Text>
|
||||
<Typography.Paragraph
|
||||
ellipsis={{
|
||||
rows: 2,
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</Typography.Paragraph>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Checkbox, Input } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Checkbox, Input, Typography } from 'antd';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
|
||||
@@ -2,8 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Spacing } from '@signozhq/design-tokens';
|
||||
import { Button, Divider, Drawer, Radio } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Button, Divider, Drawer, Radio, Typography } from 'antd';
|
||||
import type { RadioChangeEvent } from 'antd/lib';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { QueryFunctionContext, useQueries, useQuery } from 'react-query';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Spin, Switch, Table, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Spin, Switch, Table, Tooltip, Typography } from 'antd';
|
||||
import { getQueryRangeV5 } from 'api/v5/queryRange/getQueryRange';
|
||||
import { MetricRangePayloadV5, ScalarData } from 'api/v5/v5';
|
||||
import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { Skeleton, Table, TablePaginationConfig } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Skeleton, Table, TablePaginationConfig, Typography } from 'antd';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import {
|
||||
dependentServicesColumns,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useQueries } from 'react-query';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Progress, Skeleton, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Progress, Skeleton, Tooltip, Typography } from 'antd';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import {
|
||||
@@ -88,16 +87,28 @@ function DomainMetrics({
|
||||
<div className="domain-detail-drawer__endpoint">
|
||||
<div className="domain-details-grid">
|
||||
<div className="labels-row">
|
||||
<Typography.Text color="muted" className="domain-details-metadata-label">
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="domain-details-metadata-label"
|
||||
>
|
||||
EXTERNAL API
|
||||
</Typography.Text>
|
||||
<Typography.Text color="muted" className="domain-details-metadata-label">
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="domain-details-metadata-label"
|
||||
>
|
||||
AVERAGE LATENCY
|
||||
</Typography.Text>
|
||||
<Typography.Text color="muted" className="domain-details-metadata-label">
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="domain-details-metadata-label"
|
||||
>
|
||||
ERROR %
|
||||
</Typography.Text>
|
||||
<Typography.Text color="muted" className="domain-details-metadata-label">
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="domain-details-metadata-label"
|
||||
>
|
||||
LAST USED
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Progress, Skeleton, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Progress, Skeleton, Tooltip, Typography } from 'antd';
|
||||
import {
|
||||
getDisplayValue,
|
||||
getFormattedEndPointMetricsData,
|
||||
@@ -38,16 +37,28 @@ function EndPointMetrics({
|
||||
<div className="domain-detail-drawer__endpoint">
|
||||
<div className="domain-details-grid">
|
||||
<div className="labels-row">
|
||||
<Typography.Text color="muted" className="domain-details-metadata-label">
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="domain-details-metadata-label"
|
||||
>
|
||||
Rate
|
||||
</Typography.Text>
|
||||
<Typography.Text color="muted" className="domain-details-metadata-label">
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="domain-details-metadata-label"
|
||||
>
|
||||
AVERAGE LATENCY
|
||||
</Typography.Text>
|
||||
<Typography.Text color="muted" className="domain-details-metadata-label">
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="domain-details-metadata-label"
|
||||
>
|
||||
ERROR %
|
||||
</Typography.Text>
|
||||
<Typography.Text color="muted" className="domain-details-metadata-label">
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="domain-details-metadata-label"
|
||||
>
|
||||
LAST USED
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Button } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Button, Typography } from 'antd';
|
||||
import { RotateCw } from 'lucide-react';
|
||||
|
||||
import awwSnapUrl from '@/assets/Icons/awwSnap.svg';
|
||||
@@ -14,7 +13,7 @@ function ErrorState({ refetch }: { refetch: () => void }): JSX.Element {
|
||||
</div>
|
||||
<div className="error-state-text">
|
||||
<Typography.Text>Uh-oh :/ We ran into an error.</Typography.Text>
|
||||
<Typography.Text color="muted">
|
||||
<Typography.Text type="secondary">
|
||||
Please refresh this panel.
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Card, Skeleton } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Button, Card, Skeleton, Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer';
|
||||
import {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { Table } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Table, Typography } from 'antd';
|
||||
import {
|
||||
endPointStatusCodeColumns,
|
||||
getFormattedEndPointStatusCodeData,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
@@ -15,6 +14,7 @@ import {
|
||||
Table,
|
||||
TableColumnsType as ColumnsType,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import getUsage, { UsageResponsePayloadProps } from 'api/billing/getUsage';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
@@ -362,19 +362,15 @@ export default function BillingContainer(): JSX.Element {
|
||||
[apiResponse, billAmount, isLoading, isFetchingBillingData],
|
||||
);
|
||||
|
||||
const { Text } = Typography;
|
||||
const subscriptionPastDueMessage = (): JSX.Element => (
|
||||
<Typography>
|
||||
{`We were not able to process payments for your account. Please update your card details `}
|
||||
<a
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={handleBilling}
|
||||
style={{ cursor: 'pointer', color: 'var(--bg-cherry-500)' }}
|
||||
>
|
||||
<Text type="danger" onClick={handleBilling} style={{ cursor: 'pointer' }}>
|
||||
{t('here')}
|
||||
</a>
|
||||
</Text>
|
||||
{` if your payment information has changed. Email us at `}
|
||||
<Typography.Text color="muted">cloud-support@signoz.io</Typography.Text>
|
||||
<Text type="secondary">cloud-support@signoz.io</Text>
|
||||
{` otherwise. Be sure to provide this information immediately to avoid interruption to your service.`}
|
||||
</Typography>
|
||||
);
|
||||
@@ -422,7 +418,7 @@ export default function BillingContainer(): JSX.Element {
|
||||
<Typography.Text style={{ fontWeight: 500, fontSize: 18 }}>
|
||||
{t('billing')}
|
||||
</Typography.Text>
|
||||
<Typography.Text color="muted">
|
||||
<Typography.Text color={Color.BG_VANILLA_400}>
|
||||
{t('manage_billing_and_costs')}
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
@@ -476,7 +472,7 @@ export default function BillingContainer(): JSX.Element {
|
||||
|
||||
{trialInfo?.onTrial && trialInfo?.trialConvertedToSubscription && (
|
||||
<Typography.Text
|
||||
truncate={1}
|
||||
ellipsis
|
||||
style={{ fontWeight: '300', color: 'var(--bg-forest-500)', fontSize: 12 }}
|
||||
>
|
||||
{t('card_details_recieved_and_billing_info')}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Card, Flex } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Card, Flex, Typography } from 'antd';
|
||||
import Uplot from 'components/Uplot';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
@@ -209,7 +208,7 @@ export function BillingUsageGraph(props: BillingUsageGraphProps): JSX.Element {
|
||||
<Typography.Text className="total-spent-title">
|
||||
TOTAL SPENT
|
||||
</Typography.Text>
|
||||
<Typography.Text className="total-spent">
|
||||
<Typography.Text color={Color.BG_VANILLA_100} className="total-spent">
|
||||
${numberFormatter.format(billAmount)}
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Row, Tag } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Row, Tag, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
@@ -79,9 +78,7 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
|
||||
data-testid={`alert-type-card-${option.selection}`}
|
||||
>
|
||||
{option.description}{' '}
|
||||
<a
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
<Typography.Link
|
||||
onClick={(e): void => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -89,7 +86,7 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
|
||||
}}
|
||||
>
|
||||
Click here to see how to create a sample alert.
|
||||
</a>{' '}
|
||||
</Typography.Link>{' '}
|
||||
</AlertTypeCard>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Button, Select, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Button, Select, Tooltip, Typography } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import getRandomColor from 'lib/getRandomColor';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Select } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Select, Typography } from 'antd';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Button, Input, Select, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Button, Input, Select, Tooltip, Typography } from 'antd';
|
||||
import { CircleX, Trash } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Button, Flex, SelectProps, Switch } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Button, Flex, SelectProps, Switch, Typography } from 'antd';
|
||||
import type { BaseOptionType, DefaultOptionType } from 'antd/es/select';
|
||||
import { getInvolvedQueriesInTraceOperator } from 'components/QueryBuilderV2/QueryV2/TraceOperator/utils/utils';
|
||||
import { YAxisSource } from 'components/YAxisUnitSelector/types';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Switch, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Switch, Tooltip, Typography } from 'antd';
|
||||
import { Info } from 'lucide-react';
|
||||
|
||||
import { IAdvancedOptionItemProps } from '../types';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Collapse, Input } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Collapse, Input, Typography } from 'antd';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
import AdvancedOptionItem from './AdvancedOptionItem';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Button } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Button, Typography } from 'antd';
|
||||
import { useCreateAlertState } from 'container/CreateAlertV2/context';
|
||||
import { INITIAL_ADVANCED_OPTIONS_STATE } from 'container/CreateAlertV2/context/constants';
|
||||
import { IEditCustomScheduleProps } from 'container/CreateAlertV2/EvaluationSettings/types';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Input, Select, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Input, Select, Tooltip, Typography } from 'antd';
|
||||
import { Info } from 'lucide-react';
|
||||
|
||||
import { useCreateAlertState } from '../../context';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Button, DatePicker, Input, Select } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Button, DatePicker, Input, Select, Typography } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { useCreateAlertState } from 'container/CreateAlertV2/context';
|
||||
import { AdvancedOptionsState } from 'container/CreateAlertV2/context/types';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Modal } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Modal, Typography } from 'antd';
|
||||
import { Calendar, Info } from 'lucide-react';
|
||||
|
||||
import { useCreateAlertState } from '../../context';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Input, Select } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Input, Select, Typography } from 'antd';
|
||||
|
||||
import { ADVANCED_OPTIONS_TIME_UNIT_OPTIONS } from '../../context/constants';
|
||||
import {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Button } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Button, Typography } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { Check } from 'lucide-react';
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { Select, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Select, Tooltip, Typography } from 'antd';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { Info } from 'lucide-react';
|
||||
|
||||
@@ -99,9 +98,9 @@ function MultipleNotifications(): JSX.Element {
|
||||
data-testid="multiple-notifications-select"
|
||||
/>
|
||||
{isMultipleNotificationsEnabled && (
|
||||
<Typography.Text className="multiple-notifications-select-description">
|
||||
<Typography.Paragraph className="multiple-notifications-select-description">
|
||||
{groupByDescription}
|
||||
</Typography.Text>
|
||||
</Typography.Paragraph>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Input, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Input, Tooltip, Typography } from 'antd';
|
||||
import { Info } from 'lucide-react';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Input, Select } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Input, Select, Typography } from 'antd';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
import {
|
||||
|
||||
@@ -177,7 +177,11 @@ export default function CustomDomainSettings(): JSX.Element {
|
||||
if (isLoadingHosts) {
|
||||
return (
|
||||
<div className="custom-domain-card custom-domain-card--loading">
|
||||
<Skeleton active paragraph={{ rows: 1, width: '60%' }} />
|
||||
<Skeleton
|
||||
active
|
||||
title={{ width: '40%' }}
|
||||
paragraph={{ rows: 1, width: '60%' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,16 @@ import { Layout } from 'react-grid-layout';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Input, Modal, Popover, Tag, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Input,
|
||||
Modal,
|
||||
Popover,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ConfigureIcon from 'assets/Integrations/ConfigureIcon';
|
||||
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
|
||||
@@ -7,8 +7,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Select } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Select, Typography } from 'antd';
|
||||
import CustomSelect from 'components/NewSelect/CustomSelect';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
|
||||
@@ -214,7 +213,7 @@ function DynamicVariable({
|
||||
</div>
|
||||
{errorAttributeKeyMessage && (
|
||||
<div>
|
||||
<Typography.Text color="warning">
|
||||
<Typography.Text type="warning">
|
||||
{errorAttributeKeyMessage}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
@@ -5,8 +5,7 @@ import { useQuery } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { orange } from '@ant-design/colors';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Collapse, Input, Select, Switch, Tag } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Button, Collapse, Input, Select, Switch, Tag, Typography } from 'antd';
|
||||
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
|
||||
import cx from 'classnames';
|
||||
import Editor from 'components/Editor';
|
||||
@@ -486,7 +485,7 @@ function VariableItem({
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<Typography.Text color="warning">{errorNameMessage}</Typography.Text>
|
||||
<Typography.Text type="warning">{errorNameMessage}</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</VariableItemRow>
|
||||
|
||||
@@ -11,8 +11,7 @@ import {
|
||||
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
|
||||
import { arrayMove, SortableContext, useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { Button, Modal, Row, RowProps, Space, Table } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Button, Modal, Row, RowProps, Space, Table, Typography } from 'antd';
|
||||
import { VariablesSettingsTabHandle } from 'container/DashboardContainer/DashboardDescription/types';
|
||||
import { convertVariablesToDbFormat } from 'container/DashboardContainer/DashboardVariablesSelection/util';
|
||||
import { useAddDynamicVariableToPanels } from 'hooks/dashboard/useAddDynamicVariableToPanels';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Col, Input, Radio, Select, Space } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Col, Input, Radio, Select, Space, Typography } from 'antd';
|
||||
import AddTags from 'container/DashboardContainer/DashboardSettings/General/AddTags';
|
||||
import { useDashboardCursorSyncMode } from 'hooks/dashboard/useDashboardCursorSyncMode';
|
||||
import { useSyncTooltipFilterMode } from 'hooks/dashboard/useSyncTooltipFilterMode';
|
||||
|
||||
@@ -2,8 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { Checkbox, toast } from '@signozhq/ui';
|
||||
import { Button, Select } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Button, Select, Typography } from 'antd';
|
||||
import createPublicDashboardAPI from 'api/dashboard/public/createPublicDashboard';
|
||||
import revokePublicDashboardAccessAPI from 'api/dashboard/public/revokePublicDashboardAccess';
|
||||
import updatePublicDashboardAPI from 'api/dashboard/public/updatePublicDashboard';
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { memo, useMemo } from 'react';
|
||||
import { orange } from '@ant-design/colors';
|
||||
import { WarningOutlined } from '@ant-design/icons';
|
||||
import { Popover, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui';
|
||||
import { Popover, Tooltip, Typography } from 'antd';
|
||||
import { CustomMultiSelect, CustomSelect } from 'components/NewSelect';
|
||||
import { OptionData } from 'components/NewSelect/types';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user