Files
zeekay ed4424bf84 merge: upstream/main (sync, brand preserved)
# Conflicts:
#	.github/workflows/build-community.yaml
#	.github/workflows/build-enterprise.yaml
#	.github/workflows/gor-signoz-community.yaml
#	cmd/community/Dockerfile
#	cmd/community/Dockerfile.multi-arch
#	cmd/community/main.go
#	cmd/community/server.go
#	cmd/enterprise/Dockerfile
#	cmd/enterprise/Dockerfile.integration
#	cmd/enterprise/Dockerfile.with-web.integration
#	cmd/server/metastore.go
#	cmd/zap.go
#	conf/prometheus.yml
#	deploy/docker-swarm/generator/hotrod/docker-compose.yaml
#	deploy/docker-swarm/generator/infra/docker-compose.yaml
#	deploy/docker-swarm/generator/infra/otel-agent-config.yaml
#	deploy/docker-swarm/generator/infra/otel-metrics-config.yaml
#	deploy/docker/generator/hotrod/docker-compose.yaml
#	deploy/docker/generator/infra/docker-compose.yaml
#	deploy/docker/generator/infra/otel-collector-config.yaml
#	docs/contributing/go/integration.md
#	ee/anomaly/hourly.go
#	ee/anomaly/params.go
#	ee/anomaly/seasonal.go
#	ee/authn/callbackauthn/oidccallbackauthn/authn.go
#	ee/authz/openfgaauthz/provider.go
#	ee/authz/openfgaschema/base.fga
#	ee/authz/openfgaserver/server.go
#	ee/licensing/config.go
#	ee/licensing/httplicensing/provider.go
#	ee/modules/dashboard/impldashboard/module.go
#	ee/querier/handler.go
#	ee/query-service/anomaly/seasonal.go
#	ee/query-service/app/api/cloudIntegrations.go
#	ee/query-service/app/api/featureFlags.go
#	ee/query-service/app/api/license.go
#	ee/query-service/app/api/queryrange.go
#	ee/query-service/rules/anomaly.go
#	ee/query-service/rules/anomaly_test.go
#	ee/query-service/rules/manager.go
#	ee/query-service/rules/manager_test.go
#	ee/query-service/usage/manager.go
#	ee/sqlschema/postgressqlschema/provider.go
#	ee/sqlstore/postgressqlstore/dialect.go
#	ee/sqlstore/postgressqlstore/provider.go
#	ee/zeus/config.go
#	ee/zeus/httpzeus/provider.go
#	frontend/.eslintrc.cjs
#	frontend/e2e/test-plan/README.md
#	frontend/e2e/test-plan/validation-report.md
#	frontend/e2e/tests/settings/account-settings/account-settings-settings.spec.ts
#	frontend/e2e/tests/settings/api-keys/api-keys-settings.spec.ts
#	frontend/playwright.config.ts
#	frontend/public/Images/signoz-hero-image.webp
#	frontend/public/Logos/signoz-brand-logo.svg
#	frontend/scripts/generate-permissions-type.cjs
#	frontend/src/assets/Images/o11y-hero-image.webp
#	frontend/src/assets/Images/signoz-hero-image.webp
#	frontend/src/assets/Logos/hanzo-brand-logo.svg
#	frontend/src/assets/Logos/hanzo-icon.svg
#	frontend/src/assets/Logos/o11y-brand-logo.svg
#	frontend/src/assets/Logos/signoz-brand-logo.svg
#	frontend/src/assets/SigNoz-dark.svg
#	frontend/src/assets/SigNoz-white.svg
#	frontend/src/assets/signoz.svg
#	frontend/src/components/DetailsDrawer/DetailsDrawer.styles.scss
#	frontend/src/components/HostMetricsDetail/HostMetricTraces/HostMetricTraces.styles.scss
#	frontend/src/components/HostMetricsDetail/HostMetricTraces/constants.ts
#	frontend/src/components/HostMetricsDetail/HostMetricsDetail.styles.scss
#	frontend/src/components/HostMetricsDetail/HostMetricsDetails.tsx
#	frontend/src/components/HostMetricsDetail/HostMetricsLogs/NoLogsContainer.tsx
#	frontend/src/components/HostMetricsDetail/HostMetricsLogs/constants.ts
#	frontend/src/components/LogsDownloadOptionsMenu/LogsDownloadOptionsMenu.styles.scss
#	frontend/src/components/WelcomeLeftContainer/index.tsx
#	frontend/src/container/APIKeys/APIKeys.styles.scss
#	frontend/src/container/APIKeys/APIKeys.test.tsx
#	frontend/src/container/APIKeys/APIKeys.tsx
#	frontend/src/container/CloudIntegrationPage/Header/Header.tsx
#	frontend/src/container/CloudIntegrationPage/HeroSection/HeroSection.tsx
#	frontend/src/container/CloudIntegrationPage/HeroSection/components/AccountActions.style.scss
#	frontend/src/container/CloudIntegrationPage/HeroSection/components/AccountActions.tsx
#	frontend/src/container/CloudIntegrationPage/HeroSection/components/AccountSettingsModal.style.scss
#	frontend/src/container/CloudIntegrationPage/HeroSection/components/AccountSettingsModal.tsx
#	frontend/src/container/CloudIntegrationPage/HeroSection/components/CloudAccountSetupModal.style.scss
#	frontend/src/container/CloudIntegrationPage/HeroSection/components/RemoveIntegrationAccount.tsx
#	frontend/src/container/CloudIntegrationPage/ServicesSection/ConfigureServiceModal.tsx
#	frontend/src/container/CloudIntegrationPage/ServicesSection/ServicesTabs.style.scss
#	frontend/src/container/CloudIntegrationPage/ServicesSection/ServicesTabs.tsx
#	frontend/src/container/DashboardContainer/DashboardSettings/General/AddTags/AddTags.styles.scss
#	frontend/src/container/DashboardContainer/DashboardSettings/General/GeneralSettings.styles.scss
#	frontend/src/container/FormAlertRules/ChQuerySection/ChQuerySection.tsx
#	frontend/src/container/FormAlertRules/UserGuide/index.tsx
#	frontend/src/container/InfraMonitoringHosts/HostsEmptyOrIncorrectMetrics.tsx
#	frontend/src/container/InfraMonitoringHosts/InfraMonitoring.styles.scss
#	frontend/src/container/InfraMonitoringHosts/__tests__/HostsEmptyOrIncorrectMetrics.test.tsx
#	frontend/src/container/InfraMonitoringK8s/Clusters/ClusterDetails/ClusterDetails.tsx
#	frontend/src/container/InfraMonitoringK8s/Clusters/utils.tsx
#	frontend/src/container/InfraMonitoringK8s/DaemonSets/DaemonSetDetails/DaemonSetDetails.tsx
#	frontend/src/container/InfraMonitoringK8s/DaemonSets/utils.tsx
#	frontend/src/container/InfraMonitoringK8s/Deployments/DeploymentDetails/DeploymentDetails.tsx
#	frontend/src/container/InfraMonitoringK8s/Deployments/utils.tsx
#	frontend/src/container/InfraMonitoringK8s/EntityDetailsUtils/EntityEvents/entityEvents.styles.scss
#	frontend/src/container/InfraMonitoringK8s/EntityDetailsUtils/EntityTraces/entityTraces.styles.scss
#	frontend/src/container/InfraMonitoringK8s/InfraMonitoringK8s.styles.scss
#	frontend/src/container/InfraMonitoringK8s/Jobs/JobDetails/JobDetails.tsx
#	frontend/src/container/InfraMonitoringK8s/Jobs/utils.tsx
#	frontend/src/container/InfraMonitoringK8s/K8sEmptyOrIncorrectMetrics.tsx
#	frontend/src/container/InfraMonitoringK8s/K8sFiltersSidePanel/K8sFiltersSidePanel.styles.scss
#	frontend/src/container/InfraMonitoringK8s/Namespaces/NamespaceDetails/NamespaceDetails.styles.scss
#	frontend/src/container/InfraMonitoringK8s/Namespaces/NamespaceDetails/NamespaceDetails.tsx
#	frontend/src/container/InfraMonitoringK8s/Namespaces/utils.tsx
#	frontend/src/container/InfraMonitoringK8s/Nodes/NodeDetails/NodeDetails.tsx
#	frontend/src/container/InfraMonitoringK8s/Nodes/utils.tsx
#	frontend/src/container/InfraMonitoringK8s/Pods/PodDetails/PodDetails.tsx
#	frontend/src/container/InfraMonitoringK8s/StatefulSets/StatefulSetDetails/StatefulSetDetails.tsx
#	frontend/src/container/InfraMonitoringK8s/StatefulSets/utils.tsx
#	frontend/src/container/InfraMonitoringK8s/Volumes/VolumeDetails/VolumeDetails.tsx
#	frontend/src/container/InfraMonitoringK8s/Volumes/utils.tsx
#	frontend/src/container/ListAlertRules/AlertsEmptyState/AlertsEmptyState.styles.scss
#	frontend/src/container/ListAlertRules/ListAlert.tsx
#	frontend/src/container/LogsContextList/styles.ts
#	frontend/src/container/LogsExplorerList/ColumnView/ColumnView.tsx
#	frontend/src/container/LogsExplorerViews/QueryStatus.tsx
#	frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexTraces.tsx
#	frontend/src/container/MetricsExplorer/Explorer/RelatedMetrics.tsx
#	frontend/src/container/MetricsExplorer/Explorer/useGetRelatedMetricsGraphs.ts
#	frontend/src/container/MetricsExplorer/Summary/MetricTypeSearch.tsx
#	frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/ClickHouse/index.tsx
#	frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/ClickHouse/query.tsx
#	frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/index.tsx
#	frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/query.tsx
#	frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/datastore/index.tsx
#	frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/datastore/query.tsx
#	frontend/src/container/OnboardingQuestionaire/OnboardingFooter/OnboardingFooter.tsx
#	frontend/src/container/OnboardingQuestionaire/OnboardingHeader/OnboardingHeader.styles.scss
#	frontend/src/container/OnboardingQuestionaire/OnboardingHeader/OnboardingHeader.tsx
#	frontend/src/container/OnboardingV2Container/onboarding-configs/onboarding-config-with-links.json
#	frontend/src/container/QueryBuilder/QueryBuilder.tsx
#	frontend/src/container/QueryBuilder/components/Query/Query.tsx
#	frontend/src/container/RolesSettings/__tests__/utils.test.ts
#	frontend/src/container/ServiceTable/SkipOnBoardModal/index.tsx
#	frontend/src/container/TraceDetail/Missingtrace.tsx
#	frontend/src/container/TraceDetail/SelectedSpanDetails/config.ts
#	frontend/src/lib/uPlotV2/components/Tooltip/Tooltip.styles.scss
#	frontend/src/mocks-server/__mockdata__/apiKeys.ts
#	frontend/src/pages/AlertDetails/AlertHeader/AlertStatus/AlertStatus.tsx
#	frontend/src/pages/Integrations/IntegrationDetailPage/IntegrationDetailPage.styles.scss
#	frontend/src/pages/Integrations/IntegrationDetailPage/IntegrationDetailPage.tsx
#	frontend/src/pages/Integrations/IntegrationsList.tsx
#	frontend/src/pages/Integrations/utils.ts
#	frontend/src/types/api/integrations/aws.ts
#	pkg/alertmanager/o11yalertmanager/handler.go
#	pkg/apiserver/o11yapiserver/alertmanager.go
#	pkg/apiserver/o11yapiserver/cloudintegration.go
#	pkg/apiserver/o11yapiserver/inframonitoring.go
#	pkg/apiserver/o11yapiserver/llmpricingrule.go
#	pkg/apiserver/o11yapiserver/rawdataexport.go
#	pkg/apiserver/o11yapiserver/registry.go
#	pkg/apiserver/o11yapiserver/ruler.go
#	pkg/apiserver/o11yapiserver/ruler_examples.go
#	pkg/apiserver/o11yapiserver/ruler_examples_test.go
#	pkg/apiserver/o11yapiserver/rulestatehistory.go
#	pkg/apiserver/o11yapiserver/spanmapper.go
#	pkg/apiserver/o11yapiserver/tracedetail.go
#	pkg/authn/callbackauthn/googlecallbackauthn/authn.go
#	pkg/authz/openfgaauthz/provider.go
#	pkg/authz/openfgaschema/base.fga
#	pkg/authz/openfgaserver/server.go
#	pkg/authz/openfgaserver/server_test.go
#	pkg/authz/openfgaserver/sqlstore.go
#	pkg/http/middleware/api_key.go
#	pkg/http/middleware/authn.go
#	pkg/instrumentation/metric.go
#	pkg/modules/user/impluser/module.go
#	pkg/prometheus/clickhouseprometheus/client.go
#	pkg/prometheus/clickhouseprometheus/client_query_test.go
#	pkg/prometheus/clickhouseprometheus/provider.go
#	pkg/prometheus/config.go
#	pkg/prometheus/engine.go
#	pkg/prometheus/label.go
#	pkg/prometheus/prometheus.go
#	pkg/prometheus/prometheustest/provider.go
#	pkg/prometheus/prometheustest/utils_test.go
#	pkg/querier/promql_query.go
#	pkg/query-service/app/cloudintegrations/Readme.md
#	pkg/query-service/app/cloudintegrations/accountsRepo.go
#	pkg/query-service/app/cloudintegrations/constants.go
#	pkg/query-service/app/cloudintegrations/controller.go
#	pkg/query-service/app/cloudintegrations/models.go
#	pkg/query-service/app/cloudintegrations/serviceconfig_db.go
#	pkg/query-service/app/cloudintegrations/services/definitions/aws/eks/assets/dashboards/containerinsights.json
#	pkg/query-service/app/cloudintegrations/services/definitions/aws/eks/assets/dashboards/containerinsights_dot.json
#	pkg/query-service/app/cloudintegrations/services/definitions/aws/eks/assets/dashboards/overview.json
#	pkg/query-service/app/cloudintegrations/services/definitions/aws/eks/assets/dashboards/overview_dot.json
#	pkg/query-service/app/cloudintegrations/services/models.go
#	pkg/query-service/app/cloudintegrations/services/services.go
#	pkg/query-service/app/cloudintegrations/services/services_test.go
#	pkg/query-service/app/integrations/builtin_integrations/clickhouse/assets/dashboards/overview_dot.json
#	pkg/query-service/app/integrations/builtin_integrations/datastore/assets/dashboards/overview_dot.json
#	pkg/query-service/app/metricsexplorer/parser.go
#	pkg/query-service/app/metricsexplorer/summary.go
#	pkg/query-service/app/querier/querier_test.go
#	pkg/query-service/app/querier/v2/querier_test.go
#	pkg/query-service/app/summary.go
#	pkg/query-service/model/metrics_explorer/summary.go
#	pkg/query-service/rules/base_rule_test.go
#	pkg/query-service/rules/manager_test.go
#	pkg/query-service/rules/prom_rule.go
#	pkg/query-service/rules/prom_rule_task.go
#	pkg/query-service/rules/prom_rule_test.go
#	pkg/query-service/rules/threshold_rule_test.go
#	pkg/querybuilder/resourcefilter/tables.go
#	pkg/queryparser/queryfilterextractor/promql.go
#	pkg/ruler/o11yruler/handler.go
#	pkg/sqlmigration/088_migrate_ii_dashboards/clickhouse/overview.json
#	pkg/telemetrymetadata/metadata_test.go
#	pkg/telemetrystore/datastoretelemetrystore/telemetry.go
#	pkg/types/authtypes/name.go
#	pkg/types/authtypes/object.go
#	pkg/types/authtypes/selector.go
#	pkg/types/authtypes/typeable.go
#	pkg/types/authtypes/typeable_anonymous.go
#	pkg/types/authtypes/typeable_metaresource.go
#	pkg/types/authtypes/typeable_metaresources.go
#	pkg/types/authtypes/typeable_organization.go
#	pkg/types/authtypes/typeable_role.go
#	pkg/types/authtypes/typeable_user.go
#	pkg/types/authtypes/uuid.go
#	pkg/types/ctxtypes/auth.go
#	pkg/types/factor_api_key.go
#	pkg/types/integrationtypes/integration.go
#	pkg/types/roletypes/store.go
#	pkg/types/ruletypes/maintenance.go
#	pkg/types/serviceaccounttypes/service_account_role.go
#	pkg/types/serviceaccounttypes/store.go
2026-06-02 10:54:25 -07:00

774 lines
26 KiB
Go

package implservices
import (
"testing"
qbtypes "github.com/hanzoai/o11y/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/hanzoai/o11y/pkg/types/servicetypes/servicetypesv1"
"github.com/hanzoai/o11y/pkg/types/telemetrytypes"
"github.com/stretchr/testify/assert"
)
func TestBuildQueryRangeRequest(t *testing.T) {
m := &module{}
tests := []struct {
name string
req servicetypesv1.Request
wantErr string
assertOK func(t *testing.T, qr *qbtypes.QueryRangeRequest, startMs, endMs uint64)
}{
{
name: "valid with tags builds scope+filter and query",
req: servicetypesv1.Request{
Start: "1000000000", // 1s in ns -> 1000 ms
End: "2000000000", // 2s in ns -> 2000 ms
Tags: []servicetypesv1.TagFilterItem{
{Key: "service.name", Operator: "in", StringValues: []string{"frontend", "backend"}},
{Key: "env", Operator: "notin", StringValues: []string{"prod"}},
},
},
assertOK: func(t *testing.T, qr *qbtypes.QueryRangeRequest, startMs, endMs uint64) {
assert.Equal(t, uint64(1000), startMs)
assert.Equal(t, uint64(2000), endMs)
assert.Equal(t, qbtypes.RequestTypeScalar, qr.RequestType)
assert.Equal(t, 1, len(qr.CompositeQuery.Queries))
qe := qr.CompositeQuery.Queries[0]
assert.Equal(t, qbtypes.QueryTypeBuilder, qe.Type)
// Spec should be a traces builder query
spec, ok := qe.Spec.(qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation])
if !ok {
t.Fatalf("unexpected spec type: %T", qe.Spec)
}
assert.Equal(t, telemetrytypes.SignalTraces, spec.Signal)
// Filter should include both user filter and the scope expression
assert.NotNil(t, spec.Filter)
expr := spec.Filter.Expression
assert.Contains(t, expr, "service.name IN $1")
assert.Contains(t, expr, "env NOT IN $2")
assert.Contains(t, expr, "isRoot = true OR isEntryPoint = true")
// GroupBy should include service.name
if assert.Equal(t, 1, len(spec.GroupBy)) {
assert.Equal(t, "service.name", spec.GroupBy[0].Name)
}
// Aggregations should match expected expressions and aliases
if assert.Equal(t, 5, len(spec.Aggregations)) {
assert.Equal(t, "p99(duration_nano)", spec.Aggregations[0].Expression)
assert.Equal(t, "p99", spec.Aggregations[0].Alias)
assert.Equal(t, "avg(duration_nano)", spec.Aggregations[1].Expression)
assert.Equal(t, "avgDuration", spec.Aggregations[1].Alias)
assert.Equal(t, "count()", spec.Aggregations[2].Expression)
assert.Equal(t, "numCalls", spec.Aggregations[2].Alias)
assert.Equal(t, "countIf(status_code = 2)", spec.Aggregations[3].Expression)
assert.Equal(t, "numErrors", spec.Aggregations[3].Alias)
assert.Equal(t, "countIf(response_status_code >= 400 AND response_status_code < 500)", spec.Aggregations[4].Expression)
assert.Equal(t, "num4XX", spec.Aggregations[4].Alias)
}
},
},
{
name: "valid without tags uses only scope filter",
req: servicetypesv1.Request{
Start: "3000000000", // 3s ns -> 3000 ms
End: "5000000000", // 5s ns -> 5000 ms
},
assertOK: func(t *testing.T, qr *qbtypes.QueryRangeRequest, startMs, endMs uint64) {
assert.Equal(t, uint64(3000), startMs)
assert.Equal(t, uint64(5000), endMs)
qe := qr.CompositeQuery.Queries[0]
spec := qe.Spec.(qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation])
if assert.NotNil(t, spec.Filter) {
assert.Equal(t, "isRoot = true OR isEntryPoint = true", spec.Filter.Expression)
}
},
},
{
name: "invalid start",
req: servicetypesv1.Request{Start: "abc", End: "100"},
wantErr: "invalid start time",
},
{
name: "invalid end",
req: servicetypesv1.Request{Start: "100", End: "abc"},
wantErr: "invalid end time",
},
{
name: "start not before end",
req: servicetypesv1.Request{Start: "2000", End: "2000"},
wantErr: "start must be before end",
},
{
name: "start greater than end",
req: servicetypesv1.Request{Start: "2001", End: "2000"},
wantErr: "start must be before end",
},
{
name: "invalid tag: missing key -> error",
req: servicetypesv1.Request{
Start: "1000000000",
End: "2000000000",
Tags: []servicetypesv1.TagFilterItem{{Key: "", Operator: "in", StringValues: []string{"x"}}},
},
wantErr: "key is required",
},
{
name: "invalid tag: unsupported operator -> error",
req: servicetypesv1.Request{
Start: "1000000000",
End: "2000000000",
Tags: []servicetypesv1.TagFilterItem{{Key: "env", Operator: "equals", StringValues: []string{"staging"}}},
},
wantErr: "only in and notin operators are supported",
},
{
name: "invalid tag: in but no values -> error",
req: servicetypesv1.Request{
Start: "1000000000",
End: "2000000000",
Tags: []servicetypesv1.TagFilterItem{{Key: "env", Operator: "in"}},
},
wantErr: "at least one of stringValues, boolValues, or numberValues must be populated",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
qr, startMs, endMs, err := m.buildQueryRangeRequest(&tt.req)
if tt.wantErr != "" {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
return
}
assert.NoError(t, err)
if tt.assertOK != nil {
tt.assertOK(t, qr, startMs, endMs)
}
})
}
}
func TestMapQueryRangeRespToServices(t *testing.T) {
m := &module{}
groupCol := &qbtypes.ColumnDescriptor{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "service.name"},
Type: qbtypes.ColumnTypeGroup,
}
agg := func(idx int64) *qbtypes.ColumnDescriptor {
return &qbtypes.ColumnDescriptor{AggregationIndex: idx, Type: qbtypes.ColumnTypeAggregation}
}
tests := []struct {
name string
resp *qbtypes.QueryRangeResponse
startMs, endMs uint64
wantItems []*servicetypesv1.ResponseItem
wantServices []string
}{
{
name: "empty response -> no items",
resp: &qbtypes.QueryRangeResponse{
Type: qbtypes.RequestTypeScalar,
Data: qbtypes.QueryData{Results: []any{}},
},
startMs: 1000, endMs: 2000,
wantItems: []*servicetypesv1.ResponseItem{},
wantServices: []string{},
},
{
name: "no ScalarData -> no items",
resp: &qbtypes.QueryRangeResponse{
Type: qbtypes.RequestTypeScalar,
Data: qbtypes.QueryData{Results: []any{"not-scalar"}},
},
startMs: 1000, endMs: 2000,
wantItems: []*servicetypesv1.ResponseItem{},
wantServices: []string{},
},
{
name: "missing service.name column -> no items",
resp: &qbtypes.QueryRangeResponse{
Type: qbtypes.RequestTypeScalar,
Data: qbtypes.QueryData{
Results: []any{&qbtypes.ScalarData{
QueryName: "A",
Columns: []*qbtypes.ColumnDescriptor{agg(0)}, Data: [][]any{},
},
},
},
},
startMs: 1000, endMs: 2000,
wantItems: []*servicetypesv1.ResponseItem{},
wantServices: []string{},
},
{
name: "single row maps fields and rates",
resp: &qbtypes.QueryRangeResponse{
Type: qbtypes.RequestTypeScalar,
Data: qbtypes.QueryData{
Results: []any{
&qbtypes.ScalarData{
QueryName: "A",
Columns: []*qbtypes.ColumnDescriptor{groupCol, agg(0), agg(1), agg(2), agg(3), agg(4)},
Data: [][]any{{"svc-a", float64(123.0), float64(45.0), uint64(10), uint64(2), uint64(1)}},
},
},
},
},
startMs: 0, endMs: 10000, // 10s window -> callRate = 10/10=1, errorRate=20%, fourXXRate=10%
wantItems: []*servicetypesv1.ResponseItem{
{
ServiceName: "svc-a",
Percentile99: 123.0,
AvgDuration: 45.0,
NumCalls: 10,
CallRate: 1.0,
NumErrors: 2,
ErrorRate: 20.0, // in percentage
Num4XX: 1,
FourXXRate: 10.0, // in percentage
DataWarning: servicetypesv1.DataWarning{TopLevelOps: []string{}},
},
},
wantServices: []string{"svc-a"},
},
{
name: "group column in middle maps correctly",
resp: &qbtypes.QueryRangeResponse{
Type: qbtypes.RequestTypeScalar,
Data: qbtypes.QueryData{
Results: []any{&qbtypes.ScalarData{
QueryName: "A",
Columns: []*qbtypes.ColumnDescriptor{agg(0), groupCol, agg(1), agg(2), agg(3), agg(4)},
Data: [][]any{{float64(200.0), "svc-mid", float64(50.0), uint64(20), uint64(5), uint64(2)}},
},
},
},
},
startMs: 0, endMs: 10000, // 10s window -> callRate = 2, errorRate=25%, fourXXRate=10%
wantItems: []*servicetypesv1.ResponseItem{
{
ServiceName: "svc-mid",
Percentile99: 200.0,
AvgDuration: 50.0,
NumCalls: 20,
CallRate: 2.0,
NumErrors: 5,
ErrorRate: 25.0, // in percentage
Num4XX: 2,
FourXXRate: 10.0, // in percentage
DataWarning: servicetypesv1.DataWarning{TopLevelOps: []string{}},
},
},
wantServices: []string{"svc-mid"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotItems, gotServices := m.mapQueryRangeRespToServices(tt.resp, tt.startMs, tt.endMs)
assert.Equal(t, tt.wantServices, gotServices)
assert.Equal(t, len(tt.wantItems), len(gotItems))
if len(tt.wantItems) == 1 {
assert.InDelta(t, tt.wantItems[0].Percentile99, gotItems[0].Percentile99, 1e-9)
assert.InDelta(t, tt.wantItems[0].AvgDuration, gotItems[0].AvgDuration, 1e-9)
assert.Equal(t, tt.wantItems[0].NumCalls, gotItems[0].NumCalls)
assert.InDelta(t, tt.wantItems[0].CallRate, gotItems[0].CallRate, 1e-9)
assert.Equal(t, tt.wantItems[0].NumErrors, gotItems[0].NumErrors)
assert.InDelta(t, tt.wantItems[0].ErrorRate, gotItems[0].ErrorRate, 1e-9)
assert.Equal(t, tt.wantItems[0].Num4XX, gotItems[0].Num4XX)
assert.InDelta(t, tt.wantItems[0].FourXXRate, gotItems[0].FourXXRate, 1e-9)
assert.Equal(t, tt.wantItems[0].DataWarning.TopLevelOps, gotItems[0].DataWarning.TopLevelOps)
assert.Equal(t, tt.wantItems[0].ServiceName, gotItems[0].ServiceName)
}
})
}
}
func TestBuildTopOpsQueryRangeRequest(t *testing.T) {
m := &module{}
tests := []struct {
name string
req servicetypesv1.OperationsRequest
wantErr string
assertQ func(t *testing.T, qr *qbtypes.QueryRangeRequest)
}{
{
name: "with tag filters (In, NotIn) and no scope",
req: servicetypesv1.OperationsRequest{
Start: "1000000000",
End: "2000000000",
Service: "frontend",
Tags: []servicetypesv1.TagFilterItem{
{Key: "deployment.environment", Operator: "NotIn", StringValues: []string{"prod", "staging"}},
{Key: "http.method", Operator: "in", StringValues: []string{"GET"}},
},
Limit: 10,
},
assertQ: func(t *testing.T, qr *qbtypes.QueryRangeRequest) {
if assert.Equal(t, 1, len(qr.CompositeQuery.Queries)) {
qe := qr.CompositeQuery.Queries[0]
spec, ok := qe.Spec.(qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation])
if !ok {
t.Fatalf("unexpected spec type: %T", qe.Spec)
}
assert.NotNil(t, spec.Filter)
expr := spec.Filter.Expression
// service.name added first as $1, then user tags as $2, $3
assert.Contains(t, expr, "service.name IN $1")
assert.Contains(t, expr, "deployment.environment NOT IN $2")
assert.Contains(t, expr, "http.method IN $3")
assert.NotContains(t, expr, "isRoot = true OR isEntryPoint = true")
// variables populated correctly
if v, ok := qr.Variables["1"]; assert.True(t, ok) {
vals, _ := v.Value.([]any)
if assert.Equal(t, 1, len(vals)) {
assert.Equal(t, "frontend", vals[0])
}
}
if v, ok := qr.Variables["2"]; assert.True(t, ok) {
vals, _ := v.Value.([]any)
if assert.Equal(t, 2, len(vals)) {
assert.ElementsMatch(t, []any{"prod", "staging"}, vals)
}
}
if v, ok := qr.Variables["3"]; assert.True(t, ok) {
vals, _ := v.Value.([]any)
if assert.Equal(t, 1, len(vals)) {
assert.Equal(t, "GET", vals[0])
}
}
}
},
},
{
name: "valid minimal filters, no scope added",
req: servicetypesv1.OperationsRequest{
Start: "1000000000", // 1s ns -> 1000 ms
End: "4000000000", // 4s ns -> 4000 ms
Service: "cartservice",
Limit: 50,
},
assertQ: func(t *testing.T, qr *qbtypes.QueryRangeRequest) {
assert.Equal(t, qbtypes.RequestTypeScalar, qr.RequestType)
if assert.Equal(t, 1, len(qr.CompositeQuery.Queries)) {
qe := qr.CompositeQuery.Queries[0]
assert.Equal(t, qbtypes.QueryTypeBuilder, qe.Type)
spec, ok := qe.Spec.(qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation])
if !ok {
t.Fatalf("unexpected spec type: %T", qe.Spec)
}
assert.Equal(t, telemetrytypes.SignalTraces, spec.Signal)
if assert.NotNil(t, spec.Filter) {
expr := spec.Filter.Expression
// should contain only tag filter for service.name and NOT the scope expression
assert.Contains(t, expr, "service.name IN $1")
assert.NotContains(t, expr, "isRoot = true OR isEntryPoint = true")
}
if assert.Equal(t, 1, len(spec.GroupBy)) {
assert.Equal(t, "name", spec.GroupBy[0].Name)
assert.Equal(t, telemetrytypes.FieldContextSpan, spec.GroupBy[0].FieldContext)
}
if assert.Equal(t, 5, len(spec.Aggregations)) {
assert.Equal(t, "p50(duration_nano)", spec.Aggregations[0].Expression)
assert.Equal(t, "p50", spec.Aggregations[0].Alias)
assert.Equal(t, "p95(duration_nano)", spec.Aggregations[1].Expression)
assert.Equal(t, "p95", spec.Aggregations[1].Alias)
assert.Equal(t, "p99(duration_nano)", spec.Aggregations[2].Expression)
assert.Equal(t, "p99", spec.Aggregations[2].Alias)
assert.Equal(t, "count()", spec.Aggregations[3].Expression)
assert.Equal(t, "numCalls", spec.Aggregations[3].Alias)
assert.Equal(t, "countIf(status_code = 2)", spec.Aggregations[4].Expression)
assert.Equal(t, "errorCount", spec.Aggregations[4].Alias)
}
if assert.Equal(t, 1, len(spec.Order)) {
assert.Equal(t, "p99", spec.Order[0].Key.Name)
assert.Equal(t, qbtypes.OrderDirectionDesc, spec.Order[0].Direction)
}
assert.Equal(t, 50, spec.Limit)
}
},
},
{
name: "missing service -> error",
req: servicetypesv1.OperationsRequest{Start: "1", End: "2"},
wantErr: "service is required",
},
{
name: "invalid limit low",
req: servicetypesv1.OperationsRequest{Start: "1", End: "2", Service: "s", Limit: 0},
wantErr: "limit must be between 1 and 5000",
},
{
name: "invalid limit high",
req: servicetypesv1.OperationsRequest{Start: "1", End: "2", Service: "s", Limit: 5001},
wantErr: "limit must be between 1 and 5000",
},
{
name: "invalid start",
req: servicetypesv1.OperationsRequest{Start: "abc", End: "2", Service: "s"},
wantErr: "invalid start time",
},
{
name: "invalid end",
req: servicetypesv1.OperationsRequest{Start: "1", End: "abc", Service: "s"},
wantErr: "invalid end time",
},
{
name: "start not before end",
req: servicetypesv1.OperationsRequest{Start: "2", End: "2", Service: "s"},
wantErr: "start must be before end",
},
{
name: "invalid tag in top ops -> error",
req: servicetypesv1.OperationsRequest{
Start: "1000000000",
End: "2000000000",
Service: "frontend",
Limit: 10,
Tags: []servicetypesv1.TagFilterItem{{Key: "", Operator: "in", StringValues: []string{"x"}}},
},
wantErr: "key is required",
},
{
name: "invalid tag: in but no values -> error (top ops)",
req: servicetypesv1.OperationsRequest{
Start: "1000000000",
End: "2000000000",
Service: "frontend",
Tags: []servicetypesv1.TagFilterItem{{Key: "env", Operator: "in"}},
Limit: 10,
},
wantErr: "at least one of stringValues, boolValues, or numberValues must be populated",
},
{
name: "valid tag in top ops -> ok",
req: servicetypesv1.OperationsRequest{
Start: "1000000000",
End: "2000000000",
Service: "frontend",
Tags: []servicetypesv1.TagFilterItem{{Key: "deployment.environment", Operator: "in", StringValues: []string{"prod"}}},
Limit: 5,
},
assertQ: func(t *testing.T, qr *qbtypes.QueryRangeRequest) {
qe := qr.CompositeQuery.Queries[0]
spec := qe.Spec.(qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation])
assert.Contains(t, spec.Filter.Expression, "service.name IN $1")
assert.Contains(t, spec.Filter.Expression, "deployment.environment IN $2")
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
qr, err := m.buildTopOpsQueryRangeRequest(&tt.req)
if tt.wantErr != "" {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
return
}
assert.NoError(t, err)
if tt.assertQ != nil {
tt.assertQ(t, qr)
}
})
}
}
func TestMapTopOpsQueryRangeResp(t *testing.T) {
m := &module{}
nameGroup := &qbtypes.ColumnDescriptor{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "name"},
Type: qbtypes.ColumnTypeGroup,
}
agg := func(idx int64) *qbtypes.ColumnDescriptor {
return &qbtypes.ColumnDescriptor{AggregationIndex: idx, Type: qbtypes.ColumnTypeAggregation}
}
tests := []struct {
name string
resp *qbtypes.QueryRangeResponse
want []servicetypesv1.OperationItem
}{
{
name: "empty results -> empty slice",
resp: &qbtypes.QueryRangeResponse{Type: qbtypes.RequestTypeScalar, Data: qbtypes.QueryData{Results: []any{}}},
want: []servicetypesv1.OperationItem{},
},
{
name: "non-scalar result -> empty slice",
resp: &qbtypes.QueryRangeResponse{Type: qbtypes.RequestTypeScalar, Data: qbtypes.QueryData{Results: []any{"x"}}},
want: []servicetypesv1.OperationItem{},
},
{
name: "single row maps correctly",
resp: &qbtypes.QueryRangeResponse{
Type: qbtypes.RequestTypeScalar,
Data: qbtypes.QueryData{Results: []any{&qbtypes.ScalarData{
QueryName: "A",
Columns: []*qbtypes.ColumnDescriptor{nameGroup, agg(0), agg(1), agg(2), agg(3), agg(4)},
Data: [][]any{{"opA", float64(10), float64(20), float64(30), uint64(100), uint64(7)}},
}}},
},
want: []servicetypesv1.OperationItem{{
Name: "opA",
P50: 10,
P95: 20,
P99: 30,
NumCalls: 100,
ErrorCount: 7,
}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := m.mapTopOpsQueryRangeResp(tt.resp)
assert.Equal(t, tt.want, got)
})
}
}
func TestBuildEntryPointOpsQueryRangeRequest(t *testing.T) {
m := &module{}
tests := []struct {
name string
req servicetypesv1.OperationsRequest
wantErr string
assertQ func(t *testing.T, qr *qbtypes.QueryRangeRequest)
}{
{
name: "service only -> scope present, no extra filters",
req: servicetypesv1.OperationsRequest{
Start: "1000000000",
End: "2000000000",
Service: "cartservice",
// no tags
Limit: 10,
},
assertQ: func(t *testing.T, qr *qbtypes.QueryRangeRequest) {
if assert.Equal(t, 1, len(qr.CompositeQuery.Queries)) {
qe := qr.CompositeQuery.Queries[0]
spec, ok := qe.Spec.(qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation])
if !ok {
t.Fatalf("unexpected spec type: %T", qe.Spec)
}
assert.NotNil(t, spec.Filter)
expr := spec.Filter.Expression
assert.Contains(t, expr, "service.name IN $1")
assert.Contains(t, expr, "isRoot = true OR isEntryPoint = true")
// only one variable should exist
if assert.Len(t, qr.Variables, 1) {
v := qr.Variables["1"]
vals, _ := v.Value.([]any)
if assert.Equal(t, 1, len(vals)) {
assert.Equal(t, "cartservice", vals[0])
}
}
// groupBy is name (span)
if assert.Equal(t, 1, len(spec.GroupBy)) {
assert.Equal(t, "name", spec.GroupBy[0].Name)
assert.Equal(t, telemetrytypes.FieldContextSpan, spec.GroupBy[0].FieldContext)
}
}
},
},
{
name: "with filters and scope present",
req: servicetypesv1.OperationsRequest{
Start: "1000000000",
End: "3000000000",
Service: "frontend",
Tags: []servicetypesv1.TagFilterItem{
{Key: "deployment.environment", Operator: "NotIn", StringValues: []string{"prod", "staging"}},
{Key: "http.method", Operator: "in", StringValues: []string{"GET"}},
},
Limit: 25,
},
assertQ: func(t *testing.T, qr *qbtypes.QueryRangeRequest) {
if assert.Equal(t, 1, len(qr.CompositeQuery.Queries)) {
qe := qr.CompositeQuery.Queries[0]
spec, ok := qe.Spec.(qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation])
if !ok {
t.Fatalf("unexpected spec type: %T", qe.Spec)
}
assert.NotNil(t, spec.Filter)
expr := spec.Filter.Expression
assert.Contains(t, expr, "service.name IN $1")
assert.Contains(t, expr, "deployment.environment NOT IN $2")
assert.Contains(t, expr, "http.method IN $3")
assert.Contains(t, expr, "isRoot = true OR isEntryPoint = true")
if assert.Equal(t, 1, len(spec.GroupBy)) {
assert.Equal(t, "name", spec.GroupBy[0].Name)
assert.Equal(t, telemetrytypes.FieldContextSpan, spec.GroupBy[0].FieldContext)
}
if assert.Equal(t, 5, len(spec.Aggregations)) {
assert.Equal(t, "p50(duration_nano)", spec.Aggregations[0].Expression)
assert.Equal(t, "p50", spec.Aggregations[0].Alias)
assert.Equal(t, "p95(duration_nano)", spec.Aggregations[1].Expression)
assert.Equal(t, "p95", spec.Aggregations[1].Alias)
assert.Equal(t, "p99(duration_nano)", spec.Aggregations[2].Expression)
assert.Equal(t, "p99", spec.Aggregations[2].Alias)
assert.Equal(t, "count()", spec.Aggregations[3].Expression)
assert.Equal(t, "numCalls", spec.Aggregations[3].Alias)
assert.Equal(t, "countIf(status_code = 2)", spec.Aggregations[4].Expression)
assert.Equal(t, "errorCount", spec.Aggregations[4].Alias)
}
if assert.Equal(t, 1, len(spec.Order)) {
assert.Equal(t, "p99", spec.Order[0].Key.Name)
assert.Equal(t, qbtypes.OrderDirectionDesc, spec.Order[0].Direction)
}
assert.Equal(t, 25, spec.Limit)
}
},
},
{
name: "missing service -> error",
req: servicetypesv1.OperationsRequest{Start: "1", End: "2"},
wantErr: "service is required",
},
{
name: "invalid start",
req: servicetypesv1.OperationsRequest{Start: "abc", End: "2", Service: "s"},
wantErr: "invalid start time",
},
{
name: "invalid end",
req: servicetypesv1.OperationsRequest{Start: "1", End: "abc", Service: "s"},
wantErr: "invalid end time",
},
{
name: "start not before end",
req: servicetypesv1.OperationsRequest{Start: "2", End: "2", Service: "s"},
wantErr: "start must be before end",
},
{
name: "invalid limit low",
req: servicetypesv1.OperationsRequest{Start: "1", End: "2", Service: "s", Limit: 0},
wantErr: "limit must be between 1 and 5000",
},
{
name: "invalid limit high",
req: servicetypesv1.OperationsRequest{Start: "1", End: "2", Service: "s", Limit: 5001},
wantErr: "limit must be between 1 and 5000",
},
{
name: "invalid tag in entry point ops -> error",
req: servicetypesv1.OperationsRequest{
Start: "1000000000",
End: "2000000000",
Service: "cartservice",
Limit: 10,
Tags: []servicetypesv1.TagFilterItem{{Key: "", Operator: "notin", StringValues: []string{"x"}}},
},
wantErr: "key is required",
},
{
name: "invalid tag: notin but no values -> error (entry ops)",
req: servicetypesv1.OperationsRequest{
Start: "1000000000",
End: "2000000000",
Service: "cartservice",
Limit: 10,
Tags: []servicetypesv1.TagFilterItem{{Key: "env", Operator: "notin"}},
},
wantErr: "at least one of stringValues, boolValues, or numberValues must be populated",
},
{
name: "valid tag in entry point ops -> ok",
req: servicetypesv1.OperationsRequest{
Start: "1000000000",
End: "2000000000",
Service: "cartservice",
Tags: []servicetypesv1.TagFilterItem{{Key: "deployment.environment", Operator: "notin", StringValues: []string{"prod"}}},
Limit: 10,
},
assertQ: func(t *testing.T, qr *qbtypes.QueryRangeRequest) {
spec := qr.CompositeQuery.Queries[0].Spec.(qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation])
assert.Contains(t, spec.Filter.Expression, "service.name IN $1")
assert.Contains(t, spec.Filter.Expression, "deployment.environment NOT IN $2")
assert.Contains(t, spec.Filter.Expression, "isRoot = true OR isEntryPoint = true")
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
qr, err := m.buildEntryPointOpsQueryRangeRequest(&tt.req)
if tt.wantErr != "" {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
return
}
assert.NoError(t, err)
if tt.assertQ != nil {
tt.assertQ(t, qr)
}
})
}
}
func TestMapEntryPointOpsQueryRangeResp(t *testing.T) {
m := &module{}
nameGroup := &qbtypes.ColumnDescriptor{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "name"},
Type: qbtypes.ColumnTypeGroup,
}
agg := func(idx int64) *qbtypes.ColumnDescriptor {
return &qbtypes.ColumnDescriptor{AggregationIndex: idx, Type: qbtypes.ColumnTypeAggregation}
}
tests := []struct {
name string
resp *qbtypes.QueryRangeResponse
want []servicetypesv1.OperationItem
}{
{
name: "empty results -> empty slice",
resp: &qbtypes.QueryRangeResponse{Type: qbtypes.RequestTypeScalar, Data: qbtypes.QueryData{Results: []any{}}},
want: []servicetypesv1.OperationItem{},
},
{
name: "non-scalar result -> empty slice",
resp: &qbtypes.QueryRangeResponse{Type: qbtypes.RequestTypeScalar, Data: qbtypes.QueryData{Results: []any{"x"}}},
want: []servicetypesv1.OperationItem{},
},
{
name: "single row maps correctly",
resp: &qbtypes.QueryRangeResponse{
Type: qbtypes.RequestTypeScalar,
Data: qbtypes.QueryData{Results: []any{&qbtypes.ScalarData{
QueryName: "A",
Columns: []*qbtypes.ColumnDescriptor{nameGroup, agg(0), agg(1), agg(2), agg(3), agg(4)},
Data: [][]any{{"op-entry", float64(5), float64(15), float64(25), uint64(12), uint64(1)}},
}}},
},
want: []servicetypesv1.OperationItem{{
Name: "op-entry",
P50: 5,
P95: 15,
P99: 25,
NumCalls: 12,
ErrorCount: 1,
}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := m.mapEntryPointOpsQueryRangeResp(tt.resp)
assert.Equal(t, tt.want, got)
})
}
}