mirror of
https://github.com/hanzoai/o11y.git
synced 2026-10-15 12:58:33 +00:00
# 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
774 lines
26 KiB
Go
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)
|
|
})
|
|
}
|
|
}
|