mirror of
https://github.com/SigNoz/signoz.git
synced 2026-11-03 06:50:42 +00:00
Compare commits
12 Commits
nv/exec-st
...
issue_577
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d3b84b10e | ||
|
|
c0a8c2e09a | ||
|
|
285edf0442 | ||
|
|
43cee83b85 | ||
|
|
65084ba064 | ||
|
|
7fbf97429c | ||
|
|
0c6ac740a6 | ||
|
|
411717b72b | ||
|
|
3deb3351bb | ||
|
|
ddc8a5ecb7 | ||
|
|
55d892c075 | ||
|
|
89df02dac7 |
@@ -164,12 +164,12 @@ export const TILE_DRAWINGS = {
|
||||
<path d="M4 20 C 20 20, 22 12, 32 12 S 44 4, 60 4" />
|
||||
</TileSvg>
|
||||
),
|
||||
barsSideBySide: (
|
||||
barsOverlap: (
|
||||
<TileSvg>
|
||||
<rect x={10} y={10} width={7} height={12} {...FILLED} opacity={0.5} />
|
||||
<rect x={18} y={4} width={7} height={18} {...FILLED} />
|
||||
<rect x={36} y={12} width={7} height={10} {...FILLED} opacity={0.5} />
|
||||
<rect x={44} y={7} width={7} height={15} {...FILLED} />
|
||||
<rect x={14} y={6} width={11} height={16} {...FILLED} opacity={0.5} />
|
||||
<rect x={14} y={12} width={11} height={10} {...FILLED} />
|
||||
<rect x={38} y={10} width={11} height={12} {...FILLED} opacity={0.5} />
|
||||
<rect x={38} y={14} width={11} height={8} {...FILLED} />
|
||||
</TileSvg>
|
||||
),
|
||||
barsStacked: (
|
||||
|
||||
@@ -56,7 +56,7 @@ function VisualizationSection({
|
||||
value?.timePreference ?? DashboardtypesTimePreferenceDTO.global_time;
|
||||
const barLayout = value?.stackedBarChart
|
||||
? BarLayout.STACKED
|
||||
: BarLayout.SIDE_BY_SIDE;
|
||||
: BarLayout.OVERLAP;
|
||||
const stack = value?.stack ?? defaultValue?.stack;
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,15 +4,15 @@ import type { ConfigTileItem } from '../../controls/ConfigTiles/ConfigTiles';
|
||||
import { TILE_DRAWINGS } from '../../controls/drawings/tileDrawings';
|
||||
|
||||
export enum BarLayout {
|
||||
SIDE_BY_SIDE = 'side',
|
||||
OVERLAP = 'overlap',
|
||||
STACKED = 'stacked',
|
||||
}
|
||||
|
||||
export const BAR_LAYOUT_OPTIONS: ConfigTileItem<BarLayout>[] = [
|
||||
{
|
||||
value: BarLayout.SIDE_BY_SIDE,
|
||||
label: 'Side by side',
|
||||
drawing: TILE_DRAWINGS.barsSideBySide,
|
||||
value: BarLayout.OVERLAP,
|
||||
label: 'Overlap',
|
||||
drawing: TILE_DRAWINGS.barsOverlap,
|
||||
},
|
||||
{
|
||||
value: BarLayout.STACKED,
|
||||
@@ -22,8 +22,7 @@ export const BAR_LAYOUT_OPTIONS: ConfigTileItem<BarLayout>[] = [
|
||||
];
|
||||
|
||||
export const BAR_LAYOUT_HELP: Record<BarLayout, string> = {
|
||||
[BarLayout.SIDE_BY_SIDE]:
|
||||
'Each series gets its own bar, side by side, for easy comparison.',
|
||||
[BarLayout.OVERLAP]: 'Each series is drawn from zero and may hide others.',
|
||||
[BarLayout.STACKED]:
|
||||
'Bars sit on top of each other, so the height shows the total.',
|
||||
};
|
||||
|
||||
@@ -4,9 +4,13 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/llmpricingrule"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
|
||||
@@ -22,12 +26,14 @@ import (
|
||||
const unmappedModelsLookback = time.Hour
|
||||
|
||||
type module struct {
|
||||
store llmpricingruletypes.Store
|
||||
querier querier.Querier
|
||||
store llmpricingruletypes.Store
|
||||
querier querier.Querier
|
||||
telemetryMetadataStore telemetrytypes.MetadataStore
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewModule(store llmpricingruletypes.Store, querier querier.Querier) llmpricingrule.Module {
|
||||
return &module{store: store, querier: querier}
|
||||
func NewModule(store llmpricingruletypes.Store, querier querier.Querier, telemetryMetadataStore telemetrytypes.MetadataStore, providerSettings factory.ProviderSettings) llmpricingrule.Module {
|
||||
return &module{store: store, querier: querier, telemetryMetadataStore: telemetryMetadataStore, logger: providerSettings.Logger}
|
||||
}
|
||||
|
||||
func (module *module) List(ctx context.Context, orgID valuer.UUID, offset, limit int, search string, isOverride *bool) ([]*llmpricingruletypes.LLMPricingRule, int, error) {
|
||||
@@ -51,13 +57,47 @@ func (module *module) ListUnmappedModels(ctx context.Context, orgID valuer.UUID)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unmapped := make([]*llmpricingruletypes.UnmappedModel, 0, len(models))
|
||||
for _, m := range models {
|
||||
if !llmpricingruletypes.ModelMatchesAnyRule(m.ModelName, rules) {
|
||||
unmapped = append(unmapped, m)
|
||||
return llmpricingruletypes.UnmappedModels(models, rules), nil
|
||||
}
|
||||
|
||||
// Collect keeps the rule counts even when ClickHouse is unreachable: the
|
||||
// counts that depend on it are skipped so the reporter retains their last value.
|
||||
// Models come from the traces tag table rather than a span scan, so the
|
||||
// unmapped count covers the metadata lookback instead of the last hour.
|
||||
func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {
|
||||
rules, err := module.listAllRules(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats := llmpricingruletypes.NewStatsFromRules(rules)
|
||||
|
||||
selectors := aiobservabilitytypes.StatsFieldValueSelectors(time.Now())
|
||||
results := make([]*telemetrytypes.TelemetryFieldValues, len(selectors))
|
||||
var lookups sync.WaitGroup
|
||||
for i, selector := range selectors {
|
||||
lookups.Go(func() {
|
||||
fieldValues, _, err := module.telemetryMetadataStore.GetAllValues(ctx, orgID, selector)
|
||||
if err != nil {
|
||||
module.logger.DebugContext(ctx, "failed to collect gen_ai attribute stats", slog.String("key", selector.Name), errors.Attr(err))
|
||||
return
|
||||
}
|
||||
results[i] = fieldValues
|
||||
})
|
||||
}
|
||||
lookups.Wait()
|
||||
|
||||
values := make(map[string]*telemetrytypes.TelemetryFieldValues, len(selectors))
|
||||
for i, selector := range selectors {
|
||||
if results[i] != nil {
|
||||
values[selector.Name] = results[i]
|
||||
}
|
||||
}
|
||||
return unmapped, nil
|
||||
maps.Copy(stats, aiobservabilitytypes.NewStatsFromFieldValues(values))
|
||||
if models := values[aiobservabilitytypes.GenAIRequestModel]; models != nil {
|
||||
maps.Copy(stats, llmpricingruletypes.NewStatsFromModels(models.StringValues, rules))
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// CreateOrUpdate saves a batch of pricing rules. isOverride decides how a rule
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
|
||||
"github.com/SigNoz/signoz/pkg/statsreporter"
|
||||
"github.com/SigNoz/signoz/pkg/types/llmpricingruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
@@ -13,6 +14,8 @@ type Module interface {
|
||||
// Since this module interacts with OpAMP, it must implement the AgentFeature interface.
|
||||
agentConf.AgentFeature
|
||||
|
||||
statsreporter.StatsCollector
|
||||
|
||||
List(ctx context.Context, orgID valuer.UUID, offset, limit int, search string, isOverride *bool) ([]*llmpricingruletypes.LLMPricingRule, int, error)
|
||||
Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*llmpricingruletypes.LLMPricingRule, error)
|
||||
CreateOrUpdate(ctx context.Context, orgID valuer.UUID, userEmail string, rules []*llmpricingruletypes.UpdatableLLMPricingRule) (err error)
|
||||
|
||||
@@ -163,7 +163,7 @@ func NewModules(
|
||||
CloudIntegration: cloudIntegrationModule,
|
||||
TraceDetail: impltracedetail.NewModule(impltracedetail.NewTraceStore(telemetryStore, telemetryMetadataStore, fl), providerSettings, config.TraceDetail),
|
||||
SpanMapper: spanMapper,
|
||||
LLMPricingRule: impllmpricingrule.NewModule(impllmpricingrule.NewStore(sqlstore), querier),
|
||||
LLMPricingRule: impllmpricingrule.NewModule(impllmpricingrule.NewStore(sqlstore), querier, telemetryMetadataStore, providerSettings),
|
||||
Tag: tagModule,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -570,6 +570,7 @@ func New(
|
||||
cloudIntegrationModule,
|
||||
modules.LogsPipeline,
|
||||
modules.InfraMonitoring,
|
||||
modules.LLMPricingRule,
|
||||
querier,
|
||||
authz,
|
||||
}
|
||||
|
||||
55
pkg/types/aiobservabilitytypes/stats.go
Normal file
55
pkg/types/aiobservabilitytypes/stats.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package aiobservabilitytypes
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
const (
|
||||
// statsLookback reaches into the previous day; the metadata store rounds
|
||||
// the start down to a day boundary.
|
||||
statsLookback = 24 * time.Hour
|
||||
// statsModelValuesLimit caps the model list, which also feeds the
|
||||
// unmapped model count. One value is enough for the other keys.
|
||||
statsModelValuesLimit = 100
|
||||
)
|
||||
|
||||
// statsAttributeKeys are the gen_ai span attributes whose recent presence the
|
||||
// stats reporter tracks under telemetry.traces.<key>.exists.
|
||||
var statsAttributeKeys = []string{GenAIRequestModel, GenAIToolName, GenAIAgentName}
|
||||
|
||||
// StatsFieldValueSelectors look up each tracked attribute's string values
|
||||
// since the start of the previous day.
|
||||
func StatsFieldValueSelectors(now time.Time) []*telemetrytypes.FieldValueSelector {
|
||||
selectors := make([]*telemetrytypes.FieldValueSelector, 0, len(statsAttributeKeys))
|
||||
for _, key := range statsAttributeKeys {
|
||||
limit := 1
|
||||
if key == GenAIRequestModel {
|
||||
limit = statsModelValuesLimit
|
||||
}
|
||||
selectors = append(selectors, &telemetrytypes.FieldValueSelector{
|
||||
FieldKeySelector: &telemetrytypes.FieldKeySelector{
|
||||
StartUnixMilli: now.Add(-statsLookback).UnixMilli(),
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
FieldContext: telemetrytypes.FieldContextAttribute,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
Name: key,
|
||||
},
|
||||
Limit: limit,
|
||||
})
|
||||
}
|
||||
return selectors
|
||||
}
|
||||
|
||||
// NewStatsFromFieldValues reports whether each looked-up key had a value. Keys
|
||||
// absent from values are left out, so a failed lookup never reports false.
|
||||
func NewStatsFromFieldValues(values map[string]*telemetrytypes.TelemetryFieldValues) map[string]any {
|
||||
stats := make(map[string]any, len(values))
|
||||
for _, key := range statsAttributeKeys {
|
||||
if fieldValues := values[key]; fieldValues != nil {
|
||||
stats["telemetry.traces."+key+".exists"] = len(fieldValues.StringValues) > 0
|
||||
}
|
||||
}
|
||||
return stats
|
||||
}
|
||||
73
pkg/types/aiobservabilitytypes/stats_test.go
Normal file
73
pkg/types/aiobservabilitytypes/stats_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package aiobservabilitytypes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
func TestNewStatsFromFieldValues(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
values map[string]*telemetrytypes.TelemetryFieldValues
|
||||
want map[string]any
|
||||
}{
|
||||
{
|
||||
name: "NoLookups_NothingReported",
|
||||
want: map[string]any{},
|
||||
},
|
||||
{
|
||||
name: "EveryKeySeen_AllTrue",
|
||||
values: map[string]*telemetrytypes.TelemetryFieldValues{
|
||||
GenAIRequestModel: {StringValues: []string{"gpt-5-nano"}},
|
||||
GenAIToolName: {StringValues: []string{"search"}},
|
||||
GenAIAgentName: {StringValues: []string{"planner"}},
|
||||
},
|
||||
want: map[string]any{
|
||||
"telemetry.traces.gen_ai.request.model.exists": true,
|
||||
"telemetry.traces.gen_ai.tool.name.exists": true,
|
||||
"telemetry.traces.gen_ai.agent.name.exists": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "EmptyAndMissingLookups_OnlyLookedUpKeysReported",
|
||||
values: map[string]*telemetrytypes.TelemetryFieldValues{
|
||||
GenAIRequestModel: {StringValues: []string{"gpt-5-nano"}},
|
||||
GenAIToolName: {},
|
||||
GenAIOperationName: {StringValues: []string{"chat"}},
|
||||
},
|
||||
want: map[string]any{
|
||||
"telemetry.traces.gen_ai.request.model.exists": true,
|
||||
"telemetry.traces.gen_ai.tool.name.exists": false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
assert.Equal(t, testCase.want, NewStatsFromFieldValues(testCase.values))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatsFieldValueSelectors(t *testing.T) {
|
||||
now := time.Date(2026, 11, 3, 10, 0, 0, 0, time.UTC)
|
||||
|
||||
selectors := StatsFieldValueSelectors(now)
|
||||
|
||||
require.Len(t, selectors, 3)
|
||||
for _, selector := range selectors {
|
||||
assert.Equal(t, now.Add(-24*time.Hour).UnixMilli(), selector.StartUnixMilli, selector.Name)
|
||||
assert.Equal(t, telemetrytypes.SignalTraces, selector.Signal, selector.Name)
|
||||
assert.Equal(t, telemetrytypes.FieldContextAttribute, selector.FieldContext, selector.Name)
|
||||
assert.Equal(t, telemetrytypes.FieldDataTypeString, selector.FieldDataType, selector.Name)
|
||||
}
|
||||
assert.Equal(t, GenAIRequestModel, selectors[0].Name)
|
||||
assert.Equal(t, 100, selectors[0].Limit)
|
||||
assert.Equal(t, 1, selectors[1].Limit)
|
||||
assert.Equal(t, 1, selectors[2].Limit)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package dashboardtypes
|
||||
|
||||
import (
|
||||
qb "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
@@ -10,6 +11,7 @@ const (
|
||||
statKeyPanelTracesCount = "dashboard.panels.traces.count"
|
||||
statKeyPanelMetricsCount = "dashboard.panels.metrics.count"
|
||||
statKeyPanelLogsCount = "dashboard.panels.logs.count"
|
||||
statKeyPanelAICount = "dashboard.panels.ai.count"
|
||||
)
|
||||
|
||||
// panelSignalStatKeys maps a builder query's signal to the stat it contributes
|
||||
@@ -22,14 +24,17 @@ var panelSignalStatKeys = map[telemetrytypes.Signal]string{
|
||||
}
|
||||
|
||||
// NewStatsFromStorableDashboards reports the stats of stored dashboards. Rows that
|
||||
// do not decode as v2 contribute to dashboard.count only.
|
||||
// do not decode as v2 contribute to dashboard.count only. System dashboards are
|
||||
// provisioned into every org, so they are left out of every count.
|
||||
func NewStatsFromStorableDashboards(dashboards []*StorableDashboard) map[string]any {
|
||||
stats := newPanelStats()
|
||||
stats := newStats()
|
||||
|
||||
for _, dashboard := range dashboards {
|
||||
if dashboard == nil {
|
||||
if dashboard == nil || dashboard.Source == SourceSystem {
|
||||
continue
|
||||
}
|
||||
incrementStat(stats, statKeyDashboardCount)
|
||||
|
||||
dashboardV2, err := dashboard.ToDashboardV2(nil)
|
||||
if err != nil {
|
||||
continue
|
||||
@@ -37,7 +42,6 @@ func NewStatsFromStorableDashboards(dashboards []*StorableDashboard) map[string]
|
||||
addPanelStats(&dashboardV2.Spec, stats)
|
||||
}
|
||||
|
||||
stats[statKeyDashboardCount] = int64(len(dashboards))
|
||||
return stats
|
||||
}
|
||||
|
||||
@@ -45,24 +49,26 @@ func NewStatsFromStorableDashboards(dashboards []*StorableDashboard) map[string]
|
||||
// created, straight off the postable spec — the create path has no reason to make
|
||||
// a storable round-trip just to be counted.
|
||||
func NewStatsFromPostableDashboardV2(postable PostableDashboardV2) map[string]any {
|
||||
stats := newPanelStats()
|
||||
stats := newStats()
|
||||
stats[statKeyDashboardCount] = int64(1)
|
||||
addPanelStats(&postable.Spec, stats)
|
||||
|
||||
stats[statKeyDashboardCount] = int64(1)
|
||||
return stats
|
||||
}
|
||||
|
||||
func newPanelStats() map[string]any {
|
||||
func newStats() map[string]any {
|
||||
return map[string]any{
|
||||
statKeyDashboardCount: int64(0),
|
||||
statKeyPanelCount: int64(0),
|
||||
statKeyPanelTracesCount: int64(0),
|
||||
statKeyPanelMetricsCount: int64(0),
|
||||
statKeyPanelLogsCount: int64(0),
|
||||
statKeyPanelAICount: int64(0),
|
||||
}
|
||||
}
|
||||
|
||||
// addPanelStats counts the panels of a v2 spec, and each panel's queries against
|
||||
// the signal they read.
|
||||
// the signal they read. AI queries count only towards the AI stat.
|
||||
func addPanelStats(spec *DashboardSpec, stats map[string]any) {
|
||||
for _, panel := range spec.Panels {
|
||||
if panel == nil {
|
||||
@@ -76,6 +82,10 @@ func addPanelStats(spec *DashboardSpec, stats map[string]any) {
|
||||
continue
|
||||
}
|
||||
for _, envelope := range composite.Queries {
|
||||
if envelope.Type == qb.QueryTypeBuilderAI {
|
||||
incrementStat(stats, statKeyPanelAICount)
|
||||
continue
|
||||
}
|
||||
if key, ok := panelSignalStatKeys[envelope.GetSignal()]; ok {
|
||||
incrementStat(stats, key)
|
||||
}
|
||||
|
||||
@@ -117,20 +117,60 @@ func TestNewStatsFromStorableDashboardsCountsCompositeSubQueries(t *testing.T) {
|
||||
assert.Equal(t, int64(1), stats[statKeyPanelLogsCount])
|
||||
}
|
||||
|
||||
// An AI builder query is always a traces query, so it counts towards traces.
|
||||
func TestNewStatsFromStorableDashboardsCountsAIBuilderQueries(t *testing.T) {
|
||||
aiBuilder := `{
|
||||
func statsAIBuilderQuery() string {
|
||||
return `{
|
||||
"kind": "time_series",
|
||||
"spec": {"plugin": {"kind": "signoz/AIBuilderQuery", "spec": {"name": "A", "aggregations": [{"expression": "count()"}]}}}
|
||||
}`
|
||||
dashboard := newStatsStorableV2(t, `"p1": `+statsPanel(aiBuilder))
|
||||
}
|
||||
|
||||
// An AI builder query reads traces but counts only towards the AI stat.
|
||||
func TestNewStatsFromStorableDashboardsCountsAIBuilderQueries(t *testing.T) {
|
||||
dashboard := newStatsStorableV2(t, `"p1": `+statsPanel(statsAIBuilderQuery()))
|
||||
|
||||
stats := NewStatsFromStorableDashboards([]*StorableDashboard{dashboard})
|
||||
|
||||
assert.Equal(t, int64(1), stats[statKeyPanelCount])
|
||||
assert.Equal(t, int64(1), stats[statKeyPanelTracesCount])
|
||||
assert.Equal(t, int64(0), stats[statKeyPanelTracesCount])
|
||||
assert.Equal(t, int64(0), stats[statKeyPanelMetricsCount])
|
||||
assert.Equal(t, int64(0), stats[statKeyPanelLogsCount])
|
||||
assert.Equal(t, int64(1), stats[statKeyPanelAICount])
|
||||
}
|
||||
|
||||
func TestNewStatsFromStorableDashboardsCountsAICompositeSubQueries(t *testing.T) {
|
||||
composite := `{
|
||||
"kind": "time_series",
|
||||
"spec": {"plugin": {"kind": "signoz/CompositeQuery", "spec": {"queries": [
|
||||
{"type": "builder_ai_query", "spec": {"name": "A", "aggregations": [{"expression": "count()"}]}},
|
||||
{"type": "builder_query", "spec": ` + statsBuilderQuerySpec("B", "logs") + `}
|
||||
]}}}
|
||||
}`
|
||||
dashboard := newStatsStorableV2(t, `
|
||||
"p1": `+statsPanel(composite)+`,
|
||||
"p2": `+statsPanel(statsAIBuilderQuery())+`
|
||||
`)
|
||||
|
||||
stats := NewStatsFromStorableDashboards([]*StorableDashboard{dashboard})
|
||||
|
||||
assert.Equal(t, int64(2), stats[statKeyPanelCount])
|
||||
assert.Equal(t, int64(2), stats[statKeyPanelAICount])
|
||||
assert.Equal(t, int64(0), stats[statKeyPanelTracesCount])
|
||||
assert.Equal(t, int64(1), stats[statKeyPanelLogsCount])
|
||||
}
|
||||
|
||||
func TestNewStatsFromStorableDashboardsSkipsSystemDashboards(t *testing.T) {
|
||||
system := newStatsStorableV2(t, `"p1": `+statsPanel(statsAIBuilderQuery()))
|
||||
system.Source = SourceSystem
|
||||
system.Name = SystemDashboardNamePrefix + "overview"
|
||||
user := newStatsStorableV2(t, `"p1": `+statsPanel(statsBuilderQuery("logs")))
|
||||
|
||||
stats := NewStatsFromStorableDashboards([]*StorableDashboard{system, user})
|
||||
|
||||
assert.Equal(t, int64(1), stats[statKeyDashboardCount])
|
||||
assert.Equal(t, int64(1), stats[statKeyPanelCount])
|
||||
assert.Equal(t, int64(0), stats[statKeyPanelTracesCount])
|
||||
assert.Equal(t, int64(0), stats[statKeyPanelAICount])
|
||||
assert.Equal(t, int64(1), stats[statKeyPanelLogsCount])
|
||||
}
|
||||
|
||||
// promql and clickhouse queries carry no signal, so they land in the panel total
|
||||
@@ -215,6 +255,17 @@ func TestNewStatsFromPostableDashboardV2(t *testing.T) {
|
||||
assert.Equal(t, int64(1), stats[statKeyPanelLogsCount])
|
||||
assert.Equal(t, int64(1), stats[statKeyPanelTracesCount])
|
||||
assert.Equal(t, int64(0), stats[statKeyPanelMetricsCount])
|
||||
assert.Equal(t, int64(0), stats[statKeyPanelAICount])
|
||||
}
|
||||
|
||||
func TestNewStatsFromPostableDashboardV2CountsAIBuilderQueries(t *testing.T) {
|
||||
postable := newStatsPostableV2(t, `"p1": `+statsPanel(statsAIBuilderQuery()))
|
||||
|
||||
stats := NewStatsFromPostableDashboardV2(postable)
|
||||
|
||||
assert.Equal(t, int64(1), stats[statKeyDashboardCount])
|
||||
assert.Equal(t, int64(1), stats[statKeyPanelCount])
|
||||
assert.Equal(t, int64(1), stats[statKeyPanelAICount])
|
||||
}
|
||||
|
||||
func TestNewStatsFromPostableDashboardV2WithNoPanels(t *testing.T) {
|
||||
@@ -233,4 +284,5 @@ func TestNewStatsFromStorableDashboardsWithNoDashboards(t *testing.T) {
|
||||
assert.Equal(t, int64(0), stats[statKeyPanelTracesCount])
|
||||
assert.Equal(t, int64(0), stats[statKeyPanelMetricsCount])
|
||||
assert.Equal(t, int64(0), stats[statKeyPanelLogsCount])
|
||||
assert.Equal(t, int64(0), stats[statKeyPanelAICount])
|
||||
}
|
||||
|
||||
@@ -277,3 +277,13 @@ func ModelMatchesAnyRule(model string, rules []*LLMPricingRule) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func UnmappedModels(models []*UnmappedModel, rules []*LLMPricingRule) []*UnmappedModel {
|
||||
unmapped := make([]*UnmappedModel, 0, len(models))
|
||||
for _, model := range models {
|
||||
if !ModelMatchesAnyRule(model.ModelName, rules) {
|
||||
unmapped = append(unmapped, model)
|
||||
}
|
||||
}
|
||||
return unmapped
|
||||
}
|
||||
|
||||
31
pkg/types/llmpricingruletypes/stats.go
Normal file
31
pkg/types/llmpricingruletypes/stats.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package llmpricingruletypes
|
||||
|
||||
// NewStatsFromRules reports the rules users added themselves and the synced
|
||||
// defaults users pinned.
|
||||
func NewStatsFromRules(rules []*LLMPricingRule) map[string]any {
|
||||
custom, overrides := int64(0), int64(0)
|
||||
for _, rule := range rules {
|
||||
switch {
|
||||
case rule.SourceID == nil:
|
||||
custom++
|
||||
case rule.IsOverride:
|
||||
overrides++
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"llmpricingrule.custom.count": custom,
|
||||
"llmpricingrule.override.count": overrides,
|
||||
}
|
||||
}
|
||||
|
||||
func NewStatsFromModels(models []string, rules []*LLMPricingRule) map[string]any {
|
||||
unmapped := int64(0)
|
||||
for _, model := range models {
|
||||
if !ModelMatchesAnyRule(model, rules) {
|
||||
unmapped++
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]any{"llmpricingrule.unmapped_model.count": unmapped}
|
||||
}
|
||||
65
pkg/types/llmpricingruletypes/stats_test.go
Normal file
65
pkg/types/llmpricingruletypes/stats_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package llmpricingruletypes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
func TestNewStatsFromRules(t *testing.T) {
|
||||
sourceID := valuer.GenerateUUID()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
rules []*LLMPricingRule
|
||||
want map[string]any
|
||||
}{
|
||||
{
|
||||
name: "NoRules",
|
||||
want: map[string]any{
|
||||
"llmpricingrule.custom.count": int64(0),
|
||||
"llmpricingrule.override.count": int64(0),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "CustomPinnedAndSyncedRules_CountedSeparately",
|
||||
rules: []*LLMPricingRule{
|
||||
{IsOverride: true},
|
||||
{IsOverride: false},
|
||||
{SourceID: &sourceID, IsOverride: true},
|
||||
{SourceID: &sourceID, IsOverride: false},
|
||||
},
|
||||
want: map[string]any{
|
||||
"llmpricingrule.custom.count": int64(2),
|
||||
"llmpricingrule.override.count": int64(1),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
assert.Equal(t, testCase.want, NewStatsFromRules(testCase.rules))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewStatsFromModels(t *testing.T) {
|
||||
rules := []*LLMPricingRule{{ModelPattern: StringSlice{"gpt-*"}}}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
models []string
|
||||
want int64
|
||||
}{
|
||||
{name: "NoModels", want: 0},
|
||||
{name: "PricedAndUnpricedModels_UnpricedCounted", models: []string{"gpt-5-nano", "claude-haiku-5-5", "gemini-3"}, want: 2},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
assert.Equal(t, map[string]any{"llmpricingrule.unmapped_model.count": testCase.want}, NewStatsFromModels(testCase.models, rules))
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user