diff --git a/apps/web/src/components/services/dependency-drill.test.ts b/apps/web/src/components/services/dependency-drill.test.ts new file mode 100644 index 0000000000..4467959cda --- /dev/null +++ b/apps/web/src/components/services/dependency-drill.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from "vitest" +import { parseWhereClause } from "@/lib/traces/advanced-filter-sync" +import { dependencyDrillWhereClause, type DependencyDrillKind } from "./dependency-drill" + +const filtersOf = (kind: DependencyDrillKind, target: string, system: string) => + parseWhereClause(dependencyDrillWhereClause(kind, target, system)) + +describe("dependencyDrillWhereClause", () => { + it("drills a messaging destination on the current key", () => { + expect(filtersOf("messaging", "orders", "kafka").filters.attributeFilters).toEqual([ + { key: "messaging.destination.name", value: "orders" }, + ]) + }) + + it("drills a messaging edge named by its system to spans without a destination", () => { + expect(filtersOf("messaging", "kafka", "kafka").filters.attributeFilters).toEqual([ + { key: "messaging.system", value: "kafka" }, + { key: "messaging.destination.name", value: "", matchMode: "exists", negated: true }, + ]) + }) + + // The rollup merges `destination = kafka` spans into the same edge as the + // fallback spans; without OR in the where-clause only the fallback half is + // reachable. Pinned so a parser that gains OR support flips this on purpose. + it("drills only the fallback spans when a destination shares its system's name", () => { + expect(filtersOf("messaging", "kafka", "kafka").filters.attributeFilters).toContainEqual({ + key: "messaging.destination.name", + value: "", + matchMode: "exists", + negated: true, + }) + }) + + it("drills an rpc service, or the legacy system key when the edge is named by it", () => { + expect(filtersOf("rpc", "checkout.Cart", "grpc").filters.attributeFilters).toEqual([ + { key: "rpc.service", value: "checkout.Cart" }, + ]) + expect(filtersOf("rpc", "grpc", "grpc").filters.attributeFilters).toEqual([ + { key: "rpc.system", value: "grpc" }, + { key: "rpc.service", value: "", matchMode: "exists", negated: true }, + ]) + }) + + it("drills http, database and service edges on their target key", () => { + expect(filtersOf("http", "api.stripe.test", "").filters.attributeFilters).toEqual([ + { key: "server.address", value: "api.stripe.test" }, + ]) + expect(filtersOf("database", "postgresql", "").filters.attributeFilters).toEqual([ + { key: "db.system.name", value: "postgresql" }, + ]) + expect(filtersOf("service", "billing", "").filters.attributeFilters).toEqual([ + { key: "server.address", value: "billing", matchMode: "contains" }, + ]) + }) + + // A client span is almost never a trace root, and the traces page filters + // roots unless told otherwise. A dropped clause or a `SpanKind` pseudo-attribute + // used to leave every drill on an empty or unfiltered list. + it("opens the span-level list with no dropped clauses or pseudo-attributes", () => { + for (const [kind, target, system] of [ + ["service", "billing", ""], + ["database", "postgresql", ""], + ["messaging", "orders", "kafka"], + ["messaging", "kafka", "kafka"], + ["rpc", "grpc", "grpc"], + ["http", "api.stripe.test", ""], + ] as const) { + const { filters, warnings } = filtersOf(kind, target, system) + expect(warnings).toEqual([]) + expect(filters.rootOnly).toBe(false) + expect(filters.attributeFilters.map((f) => f.key)).not.toContain("SpanKind") + } + }) +}) diff --git a/apps/web/src/components/services/dependency-drill.ts b/apps/web/src/components/services/dependency-drill.ts new file mode 100644 index 0000000000..73f829840a --- /dev/null +++ b/apps/web/src/components/services/dependency-drill.ts @@ -0,0 +1,49 @@ +import { quoteWhereValue } from "@maple/domain/where-clause" + +export type DependencyDrillKind = "service" | "database" | "messaging" | "rpc" | "http" + +/** + * Where-clause that drills from a dependency edge to the spans behind it. + * + * The traces page filters root spans unless `root_only = false`, and a client + * span is almost never a root, so every drill opens the span-level list. There + * is no span-kind filter (a `SpanKind = ...` clause becomes a span-attribute + * filter that matches nothing), and the parser drops `(a OR b)` groups, so each + * drill is one aliased key that the query engine matches under every spelling. + * + * Targets mirror how the edge rollups name them: messaging and rpc fall back to + * the system when the destination or `rpc.service` is absent, so those drills + * also require the absence, otherwise they would match every destination of the + * system. The rpc system uses the legacy key because that is what the rollup + * reads today. + * + * Known gap: when a destination or rpc.service is literally named after its + * system, the rollup merges those spans and the fallback spans into one edge. + * Matching both needs an OR the where-clause parser does not support, so the + * drill shows only the fallback spans. + */ +export function dependencyDrillWhereClause( + kind: DependencyDrillKind, + target: string, + system: string, +): string { + const value = quoteWhereValue(target) + const namedBySystem = system !== "" && system === target + const spans = (...clauses: string[]) => ["root_only = false", ...clauses].join(" AND ") + switch (kind) { + case "service": + return spans(`server.address contains ${value}`) + case "database": + return spans(`db.system.name = ${value}`) + case "messaging": + return namedBySystem + ? spans(`messaging.system = ${value}`, "messaging.destination.name !exists") + : spans(`messaging.destination.name = ${value}`) + case "rpc": + return namedBySystem + ? spans(`rpc.system = ${value}`, "rpc.service !exists") + : spans(`rpc.service = ${value}`) + case "http": + return spans(`server.address = ${value}`) + } +} diff --git a/apps/web/src/components/services/service-dependencies-tab.tsx b/apps/web/src/components/services/service-dependencies-tab.tsx index 13405a2552..d6fa11b1da 100644 --- a/apps/web/src/components/services/service-dependencies-tab.tsx +++ b/apps/web/src/components/services/service-dependencies-tab.tsx @@ -9,7 +9,7 @@ import { formatLatency } from "@maple/ui/lib/format" import { normalizeTimestampInput } from "@/lib/timezone-format" import { DependencyTable, type DependencyRow } from "./dependency-table" import type { DependencyKind } from "./dependency-type-badge" -import { quoteWhereValue } from "@maple/domain/where-clause" +import { dependencyDrillWhereClause } from "./dependency-drill" interface ServiceDependenciesTabProps { serviceName: string @@ -118,7 +118,7 @@ export function ServiceDependenciesTab({ p95DurationMs: Number(edge.p95DurationMs ?? 0), hasSampling: Boolean(edge.hasSampling), samplingWeight: Number(edge.samplingWeight ?? 1), - whereClause: `SpanKind = 'Client' AND server.address ILIKE ${quoteWhereValue(`%${target}%`)}`, + whereClause: dependencyDrillWhereClause("service", target, ""), }) } @@ -148,7 +148,7 @@ export function ServiceDependenciesTab({ p95DurationMs: Number(edge.p95DurationMs ?? 0), hasSampling: Boolean(edge.hasSampling), samplingWeight: Number(edge.samplingWeight ?? 1), - whereClause: `SpanKind = 'Client' AND db.system.name = ${quoteWhereValue(target)}`, + whereClause: dependencyDrillWhereClause("database", target, ""), }) } @@ -160,12 +160,7 @@ export function ServiceDependenciesTab({ const callCount = Number(edge.callCount ?? 0) const estimated = Number(edge.estimatedCallCount ?? callCount) const system = edge.targetSystem ? String(edge.targetSystem) : "" - const whereClause = - kind === "messaging" - ? `SpanKind = 'Producer' AND messaging.destination = ${quoteWhereValue(target)}` - : kind === "rpc" - ? `SpanKind = 'Client' AND rpc.service = ${quoteWhereValue(target)}` - : `SpanKind = 'Client' AND (server.address = ${quoteWhereValue(target)} OR http.host = ${quoteWhereValue(target)})` + const whereClause = dependencyDrillWhereClause(kind, target, system) out.push({ id: `${kind}:${target}`, diff --git a/apps/web/src/components/traces/trace-peek-sheet.tsx b/apps/web/src/components/traces/trace-peek-sheet.tsx index 758e2ed712..08f4844e1b 100644 --- a/apps/web/src/components/traces/trace-peek-sheet.tsx +++ b/apps/web/src/components/traces/trace-peek-sheet.tsx @@ -350,7 +350,8 @@ function TracePeekLoaded({ const rootHttpInfo = getHttpInfo(rootSpan) const hasError = data.spans.some((s: Span) => { if (s.statusCode === "Error") return true - const httpStatus = s.spanAttributes?.["http.status_code"] + const httpStatus = + s.spanAttributes?.["http.response.status_code"] || s.spanAttributes?.["http.status_code"] const code = typeof httpStatus === "string" ? parseInt(httpStatus) : httpStatus return typeof code === "number" && code >= 500 }) @@ -388,7 +389,10 @@ function TracePeekLoaded({ traceId={traceId} hasError={hasError} httpStatusCode={rootHttpInfo?.statusCode} - deploymentEnv={rootSpan.resourceAttributes?.["deployment.environment"]} + deploymentEnv={ + rootSpan.resourceAttributes?.["deployment.environment.name"] || + rootSpan.resourceAttributes?.["deployment.environment"] + } commitSha={rootSpan.resourceAttributes?.["vcs.ref.head.revision"]} /> diff --git a/apps/web/src/routes/traces/$traceId.tsx b/apps/web/src/routes/traces/$traceId.tsx index eb88f89ae9..9f7cc6da9c 100644 --- a/apps/web/src/routes/traces/$traceId.tsx +++ b/apps/web/src/routes/traces/$traceId.tsx @@ -295,11 +295,14 @@ function TraceDetailContent({ const rootSpan = data.rootSpans[0] const rootHttpInfo = rootSpan ? getHttpInfo(rootSpan) : null - const deploymentEnv = rootSpan?.resourceAttributes?.["deployment.environment"] + const deploymentEnv = + rootSpan?.resourceAttributes?.["deployment.environment.name"] || + rootSpan?.resourceAttributes?.["deployment.environment"] const commitSha = rootSpan?.resourceAttributes?.["vcs.ref.head.revision"] const hasError = data.spans.some((s: Span) => { if (s.statusCode === "Error") return true - const httpStatus = s.spanAttributes?.["http.status_code"] + const httpStatus = + s.spanAttributes?.["http.response.status_code"] || s.spanAttributes?.["http.status_code"] if (httpStatus) { const code = typeof httpStatus === "string" ? parseInt(httpStatus) : httpStatus if (typeof code === "number" && code >= 500) return true diff --git a/packages/backend/src/dashboard-templates/application/grpc-service.ts b/packages/backend/src/dashboard-templates/application/grpc-service.ts index fb2aa836bc..274db9744d 100644 --- a/packages/backend/src/dashboard-templates/application/grpc-service.ts +++ b/packages/backend/src/dashboard-templates/application/grpc-service.ts @@ -13,7 +13,7 @@ import { } from "@maple/backend/dashboard-templates/helpers" import type { TemplateDefinition, WidgetDef } from "@maple/backend/dashboard-templates/types" -const GRPC_FILTER = `rpc.system = "grpc"` +const GRPC_FILTER = `rpc.system.name = "grpc"` function widgets(serviceName?: string): WidgetDef[] { const where = combineWhere(GRPC_FILTER, serviceWhereClause(serviceName)) diff --git a/packages/domain/src/tinybird/semconv-renames.ts b/packages/domain/src/tinybird/semconv-renames.ts index d2b460378d..c8236804c2 100644 --- a/packages/domain/src/tinybird/semconv-renames.ts +++ b/packages/domain/src/tinybird/semconv-renames.ts @@ -104,3 +104,28 @@ export function containerRuntimeExpr(resourceAttributes: MapColumnLike): Expr { + const legacy = spanAttributes.get("http.method") + return CH.if_(legacy.neq(""), legacy, spanAttributes.get("http.request.method")) +} + +/** + * HTTP response status code expression, legacy-first for the same reason as + * {@link httpRequestMethodExpr}: it has to agree with `trace_list_mv`'s + * pre-extracted `HttpStatusCode`. + */ +export function httpResponseStatusCodeExpr(spanAttributes: MapColumnLike): Expr { + const legacy = spanAttributes.get("http.status_code") + return CH.if_(legacy.neq(""), legacy, spanAttributes.get("http.response.status_code")) +} diff --git a/packages/domain/src/where-clause.test.ts b/packages/domain/src/where-clause.test.ts index de881f120e..17cfe9ce5a 100644 --- a/packages/domain/src/where-clause.test.ts +++ b/packages/domain/src/where-clause.test.ts @@ -26,6 +26,7 @@ describe("normalizeKey", () => { expect(normalizeKey("env")).toBe("deployment.environment") expect(normalizeKey("environment")).toBe("deployment.environment") expect(normalizeKey("deployment.environment")).toBe("deployment.environment") + expect(normalizeKey("deployment.environment.name")).toBe("deployment.environment") }) it("normalizes commit_sha alias", () => { diff --git a/packages/domain/src/where-clause.ts b/packages/domain/src/where-clause.ts index 930e18fff9..dabaa28990 100644 --- a/packages/domain/src/where-clause.ts +++ b/packages/domain/src/where-clause.ts @@ -39,7 +39,9 @@ export const normalizeKey = (raw: string): string => Match.value(raw.trim().toLowerCase()).pipe( Match.when("service", () => "service.name"), Match.when("span", () => "span.name"), - Match.whenOr("environment", "env", () => "deployment.environment"), + // The stable semconv key is a resource attribute too; without this it fell + // through to a span-attribute filter that matches nothing. + Match.whenOr("environment", "env", "deployment.environment.name", () => "deployment.environment"), // `deployment.commit_sha` is retired telemetry, kept only as an alias so a // saved where-clause written against it still names the commit filter. Match.whenOr("commit_sha", "deployment.commit_sha", () => "vcs.ref.head.revision"), diff --git a/packages/query-engine-integrations/src/__sql_baseline__/integrations.sql b/packages/query-engine-integrations/src/__sql_baseline__/integrations.sql index 4fc4d5aee0..a3d725bc4e 100644 --- a/packages/query-engine-integrations/src/__sql_baseline__/integrations.sql +++ b/packages/query-engine-integrations/src/__sql_baseline__/integrations.sql @@ -2810,7 +2810,7 @@ SELECT FROM attribute_values_hourly WHERE OrgId = 'org_sql_catalog' AND AttributeScope = 'span' - AND AttributeKey IN ('peer.service', 'db.system', 'db.system.name', 'messaging.system', 'rpc.system') + AND AttributeKey IN ('service.peer.name', 'peer.service', 'db.system', 'db.system.name', 'messaging.system', 'rpc.system.name', 'rpc.system') AND Hour >= '2026-01-01 10:30:00' AND Hour <= '2026-01-03 14:15:00' AND AttributeValue != '' diff --git a/packages/query-engine-integrations/src/product/setup-audit.test.ts b/packages/query-engine-integrations/src/product/setup-audit.test.ts index 2cfa7bf1a5..b4faf57255 100644 --- a/packages/query-engine-integrations/src/product/setup-audit.test.ts +++ b/packages/query-engine-integrations/src/product/setup-audit.test.ts @@ -104,7 +104,7 @@ describe("auditPeerValueInventoryQuery", () => { const { sql } = compileUnsafe(auditPeerValueInventoryQuery(), baseParams) expect(sql).toContain("AttributeScope = 'span'") expect(sql).toContain( - "AttributeKey IN ('peer.service', 'db.system', 'db.system.name', 'messaging.system', 'rpc.system')", + "AttributeKey IN ('service.peer.name', 'peer.service', 'db.system', 'db.system.name', 'messaging.system', 'rpc.system.name', 'rpc.system')", ) expect(sql).toContain("AttributeValue != ''") expect(sql).toContain("GROUP BY attributeKey, attributeValue") diff --git a/packages/query-engine-integrations/src/product/setup-audit.ts b/packages/query-engine-integrations/src/product/setup-audit.ts index 2ea0af6f50..bff750e7ee 100644 --- a/packages/query-engine-integrations/src/product/setup-audit.ts +++ b/packages/query-engine-integrations/src/product/setup-audit.ts @@ -277,10 +277,12 @@ export function auditMetricLabelCardinalityQuery(opts: { limit?: number } = {}) /** Keys whose *values* name a dependency, where inconsistent spelling fragments a service-map node. */ export const AUDIT_PEER_KEYS = [ + "service.peer.name", "peer.service", "db.system", "db.system.name", "messaging.system", + "rpc.system.name", "rpc.system", ] as const @@ -298,7 +300,7 @@ export const auditPeerValueRowSchema = Schema.Struct({ /** * The distinct values behind each dependency-naming key. Case-collision detection (`tinybird` vs - * `Tinybird`) happens app-side; the query just enumerates. Restricted to five keys, so this is a + * `Tinybird`) happens app-side; the query just enumerates. Restricted to seven keys, so this is a * bounded read of `attribute_values_hourly` rather than an attribute-value scan. */ export function auditPeerValueInventoryQuery(opts: { limit?: number } = {}) { diff --git a/packages/query-engine/src/__sql_baseline__/catalog.sql b/packages/query-engine/src/__sql_baseline__/catalog.sql index 7cc21615e9..1231252fe4 100644 --- a/packages/query-engine/src/__sql_baseline__/catalog.sql +++ b/packages/query-engine/src/__sql_baseline__/catalog.sql @@ -8564,7 +8564,7 @@ SELECT OFFSET 0 FORMAT JSON --- pipe:list_traces:contains-match:baseline [40cb839e] +-- pipe:list_traces:contains-match:baseline [e1c7d3ae] SELECT TraceId AS traceId, Timestamp AS startTime, @@ -8577,9 +8577,9 @@ SELECT SpanKind AS rootSpanKind, StatusCode AS rootSpanStatusCode, StatusMessage AS rootSpanStatusMessage, - SpanAttributes['http.method'] AS rootHttpMethod, + if(SpanAttributes['http.method'] != '', SpanAttributes['http.method'], SpanAttributes['http.request.method']) AS rootHttpMethod, SpanAttributes['http.route'] AS rootHttpRoute, - SpanAttributes['http.status_code'] AS rootHttpStatusCode, + if(SpanAttributes['http.status_code'] != '', SpanAttributes['http.status_code'], SpanAttributes['http.response.status_code']) AS rootHttpStatusCode, toJSONString(map('http.method', SpanAttributes['http.method'], 'http.request.method', SpanAttributes['http.request.method'], 'http.route', SpanAttributes['http.route'], 'http.target', SpanAttributes['http.target'], 'http.status_code', SpanAttributes['http.status_code'], 'http.response.status_code', SpanAttributes['http.response.status_code'], 'http.url', SpanAttributes['http.url'], 'url.full', SpanAttributes['url.full'], 'url.path', SpanAttributes['url.path'], 'server.address', SpanAttributes['server.address'], 'net.peer.name', SpanAttributes['net.peer.name'], 'screen.name', SpanAttributes['screen.name'])) AS rootSpanAttributes, if(StatusCode = 'Error', 1, 0) AS hasError FROM traces @@ -8602,7 +8602,7 @@ SELECT LIMIT 100 FORMAT JSON --- pipe:list_traces:contains-match:bloom [40cb839e] +-- pipe:list_traces:contains-match:bloom [e1c7d3ae] SELECT TraceId AS traceId, Timestamp AS startTime, @@ -8615,9 +8615,9 @@ SELECT SpanKind AS rootSpanKind, StatusCode AS rootSpanStatusCode, StatusMessage AS rootSpanStatusMessage, - SpanAttributes['http.method'] AS rootHttpMethod, + if(SpanAttributes['http.method'] != '', SpanAttributes['http.method'], SpanAttributes['http.request.method']) AS rootHttpMethod, SpanAttributes['http.route'] AS rootHttpRoute, - SpanAttributes['http.status_code'] AS rootHttpStatusCode, + if(SpanAttributes['http.status_code'] != '', SpanAttributes['http.status_code'], SpanAttributes['http.response.status_code']) AS rootHttpStatusCode, toJSONString(map('http.method', SpanAttributes['http.method'], 'http.request.method', SpanAttributes['http.request.method'], 'http.route', SpanAttributes['http.route'], 'http.target', SpanAttributes['http.target'], 'http.status_code', SpanAttributes['http.status_code'], 'http.response.status_code', SpanAttributes['http.response.status_code'], 'http.url', SpanAttributes['http.url'], 'url.full', SpanAttributes['url.full'], 'url.path', SpanAttributes['url.path'], 'server.address', SpanAttributes['server.address'], 'net.peer.name', SpanAttributes['net.peer.name'], 'screen.name', SpanAttributes['screen.name'])) AS rootSpanAttributes, if(StatusCode = 'Error', 1, 0) AS hasError FROM traces @@ -8640,7 +8640,7 @@ SELECT LIMIT 100 FORMAT JSON --- pipe:list_traces:contains-match:text [40cb839e] +-- pipe:list_traces:contains-match:text [e1c7d3ae] SELECT TraceId AS traceId, Timestamp AS startTime, @@ -8653,9 +8653,9 @@ SELECT SpanKind AS rootSpanKind, StatusCode AS rootSpanStatusCode, StatusMessage AS rootSpanStatusMessage, - SpanAttributes['http.method'] AS rootHttpMethod, + if(SpanAttributes['http.method'] != '', SpanAttributes['http.method'], SpanAttributes['http.request.method']) AS rootHttpMethod, SpanAttributes['http.route'] AS rootHttpRoute, - SpanAttributes['http.status_code'] AS rootHttpStatusCode, + if(SpanAttributes['http.status_code'] != '', SpanAttributes['http.status_code'], SpanAttributes['http.response.status_code']) AS rootHttpStatusCode, toJSONString(map('http.method', SpanAttributes['http.method'], 'http.request.method', SpanAttributes['http.request.method'], 'http.route', SpanAttributes['http.route'], 'http.target', SpanAttributes['http.target'], 'http.status_code', SpanAttributes['http.status_code'], 'http.response.status_code', SpanAttributes['http.response.status_code'], 'http.url', SpanAttributes['http.url'], 'url.full', SpanAttributes['url.full'], 'url.path', SpanAttributes['url.path'], 'server.address', SpanAttributes['server.address'], 'net.peer.name', SpanAttributes['net.peer.name'], 'screen.name', SpanAttributes['screen.name'])) AS rootSpanAttributes, if(StatusCode = 'Error', 1, 0) AS hasError FROM traces @@ -8678,7 +8678,7 @@ SELECT LIMIT 100 FORMAT JSON --- pipe:list_traces:default:baseline [81ae2912] +-- pipe:list_traces:default:baseline [5b987fa2] SELECT TraceId AS traceId, Timestamp AS startTime, @@ -8691,9 +8691,9 @@ SELECT SpanKind AS rootSpanKind, StatusCode AS rootSpanStatusCode, StatusMessage AS rootSpanStatusMessage, - SpanAttributes['http.method'] AS rootHttpMethod, + if(SpanAttributes['http.method'] != '', SpanAttributes['http.method'], SpanAttributes['http.request.method']) AS rootHttpMethod, SpanAttributes['http.route'] AS rootHttpRoute, - SpanAttributes['http.status_code'] AS rootHttpStatusCode, + if(SpanAttributes['http.status_code'] != '', SpanAttributes['http.status_code'], SpanAttributes['http.response.status_code']) AS rootHttpStatusCode, toJSONString(map('http.method', SpanAttributes['http.method'], 'http.request.method', SpanAttributes['http.request.method'], 'http.route', SpanAttributes['http.route'], 'http.target', SpanAttributes['http.target'], 'http.status_code', SpanAttributes['http.status_code'], 'http.response.status_code', SpanAttributes['http.response.status_code'], 'http.url', SpanAttributes['http.url'], 'url.full', SpanAttributes['url.full'], 'url.path', SpanAttributes['url.path'], 'server.address', SpanAttributes['server.address'], 'net.peer.name', SpanAttributes['net.peer.name'], 'screen.name', SpanAttributes['screen.name'])) AS rootSpanAttributes, if(StatusCode = 'Error', 1, 0) AS hasError FROM traces @@ -8714,7 +8714,7 @@ SELECT LIMIT 100 FORMAT JSON --- pipe:list_traces:default:bloom [81ae2912] +-- pipe:list_traces:default:bloom [5b987fa2] SELECT TraceId AS traceId, Timestamp AS startTime, @@ -8727,9 +8727,9 @@ SELECT SpanKind AS rootSpanKind, StatusCode AS rootSpanStatusCode, StatusMessage AS rootSpanStatusMessage, - SpanAttributes['http.method'] AS rootHttpMethod, + if(SpanAttributes['http.method'] != '', SpanAttributes['http.method'], SpanAttributes['http.request.method']) AS rootHttpMethod, SpanAttributes['http.route'] AS rootHttpRoute, - SpanAttributes['http.status_code'] AS rootHttpStatusCode, + if(SpanAttributes['http.status_code'] != '', SpanAttributes['http.status_code'], SpanAttributes['http.response.status_code']) AS rootHttpStatusCode, toJSONString(map('http.method', SpanAttributes['http.method'], 'http.request.method', SpanAttributes['http.request.method'], 'http.route', SpanAttributes['http.route'], 'http.target', SpanAttributes['http.target'], 'http.status_code', SpanAttributes['http.status_code'], 'http.response.status_code', SpanAttributes['http.response.status_code'], 'http.url', SpanAttributes['http.url'], 'url.full', SpanAttributes['url.full'], 'url.path', SpanAttributes['url.path'], 'server.address', SpanAttributes['server.address'], 'net.peer.name', SpanAttributes['net.peer.name'], 'screen.name', SpanAttributes['screen.name'])) AS rootSpanAttributes, if(StatusCode = 'Error', 1, 0) AS hasError FROM traces @@ -8750,7 +8750,7 @@ SELECT LIMIT 100 FORMAT JSON --- pipe:list_traces:default:text [81ae2912] +-- pipe:list_traces:default:text [5b987fa2] SELECT TraceId AS traceId, Timestamp AS startTime, @@ -8763,9 +8763,9 @@ SELECT SpanKind AS rootSpanKind, StatusCode AS rootSpanStatusCode, StatusMessage AS rootSpanStatusMessage, - SpanAttributes['http.method'] AS rootHttpMethod, + if(SpanAttributes['http.method'] != '', SpanAttributes['http.method'], SpanAttributes['http.request.method']) AS rootHttpMethod, SpanAttributes['http.route'] AS rootHttpRoute, - SpanAttributes['http.status_code'] AS rootHttpStatusCode, + if(SpanAttributes['http.status_code'] != '', SpanAttributes['http.status_code'], SpanAttributes['http.response.status_code']) AS rootHttpStatusCode, toJSONString(map('http.method', SpanAttributes['http.method'], 'http.request.method', SpanAttributes['http.request.method'], 'http.route', SpanAttributes['http.route'], 'http.target', SpanAttributes['http.target'], 'http.status_code', SpanAttributes['http.status_code'], 'http.response.status_code', SpanAttributes['http.response.status_code'], 'http.url', SpanAttributes['http.url'], 'url.full', SpanAttributes['url.full'], 'url.path', SpanAttributes['url.path'], 'server.address', SpanAttributes['server.address'], 'net.peer.name', SpanAttributes['net.peer.name'], 'screen.name', SpanAttributes['screen.name'])) AS rootSpanAttributes, if(StatusCode = 'Error', 1, 0) AS hasError FROM traces @@ -8786,7 +8786,7 @@ SELECT LIMIT 100 FORMAT JSON --- pipe:list_traces:filtered:baseline [3ad9d1ec] +-- pipe:list_traces:filtered:baseline [8cdab2fc] SELECT TraceId AS traceId, Timestamp AS startTime, @@ -8799,9 +8799,9 @@ SELECT SpanKind AS rootSpanKind, StatusCode AS rootSpanStatusCode, StatusMessage AS rootSpanStatusMessage, - SpanAttributes['http.method'] AS rootHttpMethod, + if(SpanAttributes['http.method'] != '', SpanAttributes['http.method'], SpanAttributes['http.request.method']) AS rootHttpMethod, SpanAttributes['http.route'] AS rootHttpRoute, - SpanAttributes['http.status_code'] AS rootHttpStatusCode, + if(SpanAttributes['http.status_code'] != '', SpanAttributes['http.status_code'], SpanAttributes['http.response.status_code']) AS rootHttpStatusCode, toJSONString(map('http.method', SpanAttributes['http.method'], 'http.request.method', SpanAttributes['http.request.method'], 'http.route', SpanAttributes['http.route'], 'http.target', SpanAttributes['http.target'], 'http.status_code', SpanAttributes['http.status_code'], 'http.response.status_code', SpanAttributes['http.response.status_code'], 'http.url', SpanAttributes['http.url'], 'url.full', SpanAttributes['url.full'], 'url.path', SpanAttributes['url.path'], 'server.address', SpanAttributes['server.address'], 'net.peer.name', SpanAttributes['net.peer.name'], 'screen.name', SpanAttributes['screen.name'])) AS rootSpanAttributes, if(StatusCode = 'Error', 1, 0) AS hasError FROM traces @@ -8838,7 +8838,7 @@ SELECT LIMIT 25 FORMAT JSON --- pipe:list_traces:filtered:bloom [2d366eb8] +-- pipe:list_traces:filtered:bloom [08b94a48] SELECT TraceId AS traceId, Timestamp AS startTime, @@ -8851,9 +8851,9 @@ SELECT SpanKind AS rootSpanKind, StatusCode AS rootSpanStatusCode, StatusMessage AS rootSpanStatusMessage, - SpanAttributes['http.method'] AS rootHttpMethod, + if(SpanAttributes['http.method'] != '', SpanAttributes['http.method'], SpanAttributes['http.request.method']) AS rootHttpMethod, SpanAttributes['http.route'] AS rootHttpRoute, - SpanAttributes['http.status_code'] AS rootHttpStatusCode, + if(SpanAttributes['http.status_code'] != '', SpanAttributes['http.status_code'], SpanAttributes['http.response.status_code']) AS rootHttpStatusCode, toJSONString(map('http.method', SpanAttributes['http.method'], 'http.request.method', SpanAttributes['http.request.method'], 'http.route', SpanAttributes['http.route'], 'http.target', SpanAttributes['http.target'], 'http.status_code', SpanAttributes['http.status_code'], 'http.response.status_code', SpanAttributes['http.response.status_code'], 'http.url', SpanAttributes['http.url'], 'url.full', SpanAttributes['url.full'], 'url.path', SpanAttributes['url.path'], 'server.address', SpanAttributes['server.address'], 'net.peer.name', SpanAttributes['net.peer.name'], 'screen.name', SpanAttributes['screen.name'])) AS rootSpanAttributes, if(StatusCode = 'Error', 1, 0) AS hasError FROM traces @@ -8890,7 +8890,7 @@ SELECT LIMIT 25 FORMAT JSON --- pipe:list_traces:filtered:text [ff504ae4] +-- pipe:list_traces:filtered:text [3cbe65b4] SELECT TraceId AS traceId, Timestamp AS startTime, @@ -8903,9 +8903,9 @@ SELECT SpanKind AS rootSpanKind, StatusCode AS rootSpanStatusCode, StatusMessage AS rootSpanStatusMessage, - SpanAttributes['http.method'] AS rootHttpMethod, + if(SpanAttributes['http.method'] != '', SpanAttributes['http.method'], SpanAttributes['http.request.method']) AS rootHttpMethod, SpanAttributes['http.route'] AS rootHttpRoute, - SpanAttributes['http.status_code'] AS rootHttpStatusCode, + if(SpanAttributes['http.status_code'] != '', SpanAttributes['http.status_code'], SpanAttributes['http.response.status_code']) AS rootHttpStatusCode, toJSONString(map('http.method', SpanAttributes['http.method'], 'http.request.method', SpanAttributes['http.request.method'], 'http.route', SpanAttributes['http.route'], 'http.target', SpanAttributes['http.target'], 'http.status_code', SpanAttributes['http.status_code'], 'http.response.status_code', SpanAttributes['http.response.status_code'], 'http.url', SpanAttributes['http.url'], 'url.full', SpanAttributes['url.full'], 'url.path', SpanAttributes['url.path'], 'server.address', SpanAttributes['server.address'], 'net.peer.name', SpanAttributes['net.peer.name'], 'screen.name', SpanAttributes['screen.name'])) AS rootSpanAttributes, if(StatusCode = 'Error', 1, 0) AS hasError FROM traces @@ -13518,7 +13518,7 @@ SELECT ) AS errorCount_tiers FORMAT JSON --- spec:traces-list-grouped-attr-fallback:baseline [6b9a4329] +-- spec:traces-list-grouped-attr-fallback:baseline [17176ec5] SELECT TraceId AS traceId, argMin(Timestamp, (if(ParentSpanId = '', 0, 1), Timestamp)) AS startTime, @@ -13531,9 +13531,9 @@ SELECT argMin(SpanName, (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootSpanName, argMin(SpanKind, (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootSpanKind, argMin(StatusCode, (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootSpanStatusCode, - argMin(SpanAttributes['http.method'], (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpMethod, + argMin(if(SpanAttributes['http.method'] != '', SpanAttributes['http.method'], SpanAttributes['http.request.method']), (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpMethod, argMin(SpanAttributes['http.route'], (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpRoute, - argMin(SpanAttributes['http.status_code'], (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpStatusCode, + argMin(if(SpanAttributes['http.status_code'] != '', SpanAttributes['http.status_code'], SpanAttributes['http.response.status_code']), (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpStatusCode, argMin(toJSONString(map('http.method', SpanAttributes['http.method'], 'http.request.method', SpanAttributes['http.request.method'], 'http.route', SpanAttributes['http.route'], 'http.target', SpanAttributes['http.target'], 'http.status_code', SpanAttributes['http.status_code'], 'http.response.status_code', SpanAttributes['http.response.status_code'], 'http.url', SpanAttributes['http.url'], 'url.full', SpanAttributes['url.full'], 'url.path', SpanAttributes['url.path'], 'server.address', SpanAttributes['server.address'], 'net.peer.name', SpanAttributes['net.peer.name'], 'screen.name', SpanAttributes['screen.name'])), (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootSpanAttributes, if(argMin(StatusCode, (if(ParentSpanId = '', 0, 1), Timestamp)) = 'Error', 1, 0) AS hasError FROM trace_detail_spans @@ -13559,7 +13559,7 @@ SELECT LIMIT 50 FORMAT JSON --- spec:traces-list-grouped-attr-fallback:bloom [b78292d3] +-- spec:traces-list-grouped-attr-fallback:bloom [89660e07] SELECT TraceId AS traceId, argMin(Timestamp, (if(ParentSpanId = '', 0, 1), Timestamp)) AS startTime, @@ -13572,9 +13572,9 @@ SELECT argMin(SpanName, (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootSpanName, argMin(SpanKind, (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootSpanKind, argMin(StatusCode, (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootSpanStatusCode, - argMin(SpanAttributes['http.method'], (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpMethod, + argMin(if(SpanAttributes['http.method'] != '', SpanAttributes['http.method'], SpanAttributes['http.request.method']), (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpMethod, argMin(SpanAttributes['http.route'], (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpRoute, - argMin(SpanAttributes['http.status_code'], (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpStatusCode, + argMin(if(SpanAttributes['http.status_code'] != '', SpanAttributes['http.status_code'], SpanAttributes['http.response.status_code']), (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpStatusCode, argMin(toJSONString(map('http.method', SpanAttributes['http.method'], 'http.request.method', SpanAttributes['http.request.method'], 'http.route', SpanAttributes['http.route'], 'http.target', SpanAttributes['http.target'], 'http.status_code', SpanAttributes['http.status_code'], 'http.response.status_code', SpanAttributes['http.response.status_code'], 'http.url', SpanAttributes['http.url'], 'url.full', SpanAttributes['url.full'], 'url.path', SpanAttributes['url.path'], 'server.address', SpanAttributes['server.address'], 'net.peer.name', SpanAttributes['net.peer.name'], 'screen.name', SpanAttributes['screen.name'])), (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootSpanAttributes, if(argMin(StatusCode, (if(ParentSpanId = '', 0, 1), Timestamp)) = 'Error', 1, 0) AS hasError FROM trace_detail_spans @@ -13600,7 +13600,7 @@ SELECT LIMIT 50 FORMAT JSON --- spec:traces-list-grouped-attr-fallback:text [b34bb97f] +-- spec:traces-list-grouped-attr-fallback:text [948dcec3] SELECT TraceId AS traceId, argMin(Timestamp, (if(ParentSpanId = '', 0, 1), Timestamp)) AS startTime, @@ -13613,9 +13613,9 @@ SELECT argMin(SpanName, (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootSpanName, argMin(SpanKind, (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootSpanKind, argMin(StatusCode, (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootSpanStatusCode, - argMin(SpanAttributes['http.method'], (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpMethod, + argMin(if(SpanAttributes['http.method'] != '', SpanAttributes['http.method'], SpanAttributes['http.request.method']), (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpMethod, argMin(SpanAttributes['http.route'], (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpRoute, - argMin(SpanAttributes['http.status_code'], (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpStatusCode, + argMin(if(SpanAttributes['http.status_code'] != '', SpanAttributes['http.status_code'], SpanAttributes['http.response.status_code']), (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpStatusCode, argMin(toJSONString(map('http.method', SpanAttributes['http.method'], 'http.request.method', SpanAttributes['http.request.method'], 'http.route', SpanAttributes['http.route'], 'http.target', SpanAttributes['http.target'], 'http.status_code', SpanAttributes['http.status_code'], 'http.response.status_code', SpanAttributes['http.response.status_code'], 'http.url', SpanAttributes['http.url'], 'url.full', SpanAttributes['url.full'], 'url.path', SpanAttributes['url.path'], 'server.address', SpanAttributes['server.address'], 'net.peer.name', SpanAttributes['net.peer.name'], 'screen.name', SpanAttributes['screen.name'])), (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootSpanAttributes, if(argMin(StatusCode, (if(ParentSpanId = '', 0, 1), Timestamp)) = 'Error', 1, 0) AS hasError FROM trace_detail_spans @@ -13641,7 +13641,7 @@ SELECT LIMIT 50 FORMAT JSON --- spec:traces-list-grouped-duration-sort:baseline [093a8beb] +-- spec:traces-list-grouped-duration-sort:baseline [a37a427f] SELECT TraceId AS traceId, argMin(Timestamp, (if(ParentSpanId = '', 0, 1), Timestamp)) AS startTime, @@ -13654,9 +13654,9 @@ SELECT argMin(SpanName, (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootSpanName, argMin(SpanKind, (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootSpanKind, argMin(StatusCode, (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootSpanStatusCode, - argMin(SpanAttributes['http.method'], (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpMethod, + argMin(if(SpanAttributes['http.method'] != '', SpanAttributes['http.method'], SpanAttributes['http.request.method']), (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpMethod, argMin(SpanAttributes['http.route'], (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpRoute, - argMin(SpanAttributes['http.status_code'], (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpStatusCode, + argMin(if(SpanAttributes['http.status_code'] != '', SpanAttributes['http.status_code'], SpanAttributes['http.response.status_code']), (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpStatusCode, argMin(toJSONString(map('http.method', SpanAttributes['http.method'], 'http.request.method', SpanAttributes['http.request.method'], 'http.route', SpanAttributes['http.route'], 'http.target', SpanAttributes['http.target'], 'http.status_code', SpanAttributes['http.status_code'], 'http.response.status_code', SpanAttributes['http.response.status_code'], 'http.url', SpanAttributes['http.url'], 'url.full', SpanAttributes['url.full'], 'url.path', SpanAttributes['url.path'], 'server.address', SpanAttributes['server.address'], 'net.peer.name', SpanAttributes['net.peer.name'], 'screen.name', SpanAttributes['screen.name'])), (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootSpanAttributes, if(argMin(StatusCode, (if(ParentSpanId = '', 0, 1), Timestamp)) = 'Error', 1, 0) AS hasError FROM trace_detail_spans @@ -13681,7 +13681,7 @@ SELECT LIMIT 50 FORMAT JSON --- spec:traces-list-grouped:baseline [b3702242] +-- spec:traces-list-grouped:baseline [f4908fbe] SELECT TraceId AS traceId, argMin(Timestamp, (if(ParentSpanId = '', 0, 1), Timestamp)) AS startTime, @@ -13694,9 +13694,9 @@ SELECT argMin(SpanName, (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootSpanName, argMin(SpanKind, (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootSpanKind, argMin(StatusCode, (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootSpanStatusCode, - argMin(SpanAttributes['http.method'], (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpMethod, + argMin(if(SpanAttributes['http.method'] != '', SpanAttributes['http.method'], SpanAttributes['http.request.method']), (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpMethod, argMin(SpanAttributes['http.route'], (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpRoute, - argMin(SpanAttributes['http.status_code'], (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpStatusCode, + argMin(if(SpanAttributes['http.status_code'] != '', SpanAttributes['http.status_code'], SpanAttributes['http.response.status_code']), (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpStatusCode, argMin(toJSONString(map('http.method', SpanAttributes['http.method'], 'http.request.method', SpanAttributes['http.request.method'], 'http.route', SpanAttributes['http.route'], 'http.target', SpanAttributes['http.target'], 'http.status_code', SpanAttributes['http.status_code'], 'http.response.status_code', SpanAttributes['http.response.status_code'], 'http.url', SpanAttributes['http.url'], 'url.full', SpanAttributes['url.full'], 'url.path', SpanAttributes['url.path'], 'server.address', SpanAttributes['server.address'], 'net.peer.name', SpanAttributes['net.peer.name'], 'screen.name', SpanAttributes['screen.name'])), (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootSpanAttributes, if(argMin(StatusCode, (if(ParentSpanId = '', 0, 1), Timestamp)) = 'Error', 1, 0) AS hasError FROM trace_detail_spans @@ -13720,7 +13720,7 @@ SELECT LIMIT 50 FORMAT JSON --- spec:traces-list-grouped:bloom [b3702242] +-- spec:traces-list-grouped:bloom [f4908fbe] SELECT TraceId AS traceId, argMin(Timestamp, (if(ParentSpanId = '', 0, 1), Timestamp)) AS startTime, @@ -13733,9 +13733,9 @@ SELECT argMin(SpanName, (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootSpanName, argMin(SpanKind, (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootSpanKind, argMin(StatusCode, (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootSpanStatusCode, - argMin(SpanAttributes['http.method'], (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpMethod, + argMin(if(SpanAttributes['http.method'] != '', SpanAttributes['http.method'], SpanAttributes['http.request.method']), (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpMethod, argMin(SpanAttributes['http.route'], (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpRoute, - argMin(SpanAttributes['http.status_code'], (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpStatusCode, + argMin(if(SpanAttributes['http.status_code'] != '', SpanAttributes['http.status_code'], SpanAttributes['http.response.status_code']), (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpStatusCode, argMin(toJSONString(map('http.method', SpanAttributes['http.method'], 'http.request.method', SpanAttributes['http.request.method'], 'http.route', SpanAttributes['http.route'], 'http.target', SpanAttributes['http.target'], 'http.status_code', SpanAttributes['http.status_code'], 'http.response.status_code', SpanAttributes['http.response.status_code'], 'http.url', SpanAttributes['http.url'], 'url.full', SpanAttributes['url.full'], 'url.path', SpanAttributes['url.path'], 'server.address', SpanAttributes['server.address'], 'net.peer.name', SpanAttributes['net.peer.name'], 'screen.name', SpanAttributes['screen.name'])), (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootSpanAttributes, if(argMin(StatusCode, (if(ParentSpanId = '', 0, 1), Timestamp)) = 'Error', 1, 0) AS hasError FROM trace_detail_spans @@ -13759,7 +13759,7 @@ SELECT LIMIT 50 FORMAT JSON --- spec:traces-list-grouped:text [b3702242] +-- spec:traces-list-grouped:text [f4908fbe] SELECT TraceId AS traceId, argMin(Timestamp, (if(ParentSpanId = '', 0, 1), Timestamp)) AS startTime, @@ -13772,9 +13772,9 @@ SELECT argMin(SpanName, (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootSpanName, argMin(SpanKind, (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootSpanKind, argMin(StatusCode, (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootSpanStatusCode, - argMin(SpanAttributes['http.method'], (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpMethod, + argMin(if(SpanAttributes['http.method'] != '', SpanAttributes['http.method'], SpanAttributes['http.request.method']), (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpMethod, argMin(SpanAttributes['http.route'], (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpRoute, - argMin(SpanAttributes['http.status_code'], (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpStatusCode, + argMin(if(SpanAttributes['http.status_code'] != '', SpanAttributes['http.status_code'], SpanAttributes['http.response.status_code']), (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootHttpStatusCode, argMin(toJSONString(map('http.method', SpanAttributes['http.method'], 'http.request.method', SpanAttributes['http.request.method'], 'http.route', SpanAttributes['http.route'], 'http.target', SpanAttributes['http.target'], 'http.status_code', SpanAttributes['http.status_code'], 'http.response.status_code', SpanAttributes['http.response.status_code'], 'http.url', SpanAttributes['http.url'], 'url.full', SpanAttributes['url.full'], 'url.path', SpanAttributes['url.path'], 'server.address', SpanAttributes['server.address'], 'net.peer.name', SpanAttributes['net.peer.name'], 'screen.name', SpanAttributes['screen.name'])), (if(ParentSpanId = '', 0, 1), Timestamp)) AS rootSpanAttributes, if(argMin(StatusCode, (if(ParentSpanId = '', 0, 1), Timestamp)) = 'Error', 1, 0) AS hasError FROM trace_detail_spans diff --git a/packages/query-engine/src/ch/ch.test.ts b/packages/query-engine/src/ch/ch.test.ts index faa76524b2..8a0355591c 100644 --- a/packages/query-engine/src/ch/ch.test.ts +++ b/packages/query-engine/src/ch/ch.test.ts @@ -734,9 +734,62 @@ describe("tracesBreakdownQuery", () => { const { sql } = compileUnsafe(q, baseParams) expect(sql).toContain("FROM traces") expect(sql).not.toContain("FROM service_overview_spans") - expect(sql).toContain("SpanAttributes['http.method'] AS name") + expect(sql).toContain( + "if(SpanAttributes['http.method'] != '', SpanAttributes['http.method'], SpanAttributes['http.request.method']) AS name", + ) + }) + + it("groups by environment under either semconv spelling on the raw path", () => { + const q = tracesBreakdownQuery({ metric: "count", groupBy: "environment" }) + const { sql } = compileUnsafe(q, baseParams) + expect(sql).toContain("FROM traces") + expect(sql).toContain( + "coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS name", + ) }) + it("groups a timeseries by http_method under either semconv spelling", () => { + const q = tracesTimeseriesQuery({ + metric: "count", + needsSampling: false, + groupBy: ["http_method"], + bucketSeconds: 300, + }) + const { sql } = compileUnsafe(q, { ...baseParams, bucketSeconds: 300 }) + expect(sql).toContain( + "if(SpanAttributes['http.method'] != '', SpanAttributes['http.method'], SpanAttributes['http.request.method'])", + ) + }) + + it("matches every http target the service map names when filtering on server.address", () => { + const q = tracesListQuery({ + attributeFilters: [{ key: "server.address", value: "x", mode: "equals" }], + }) + const { sql } = compileUnsafe(q, baseParams) + expect(sql).toContain( + "if(SpanAttributes['server.address'] != '', SpanAttributes['server.address'], if(SpanAttributes['http.host'] != '', SpanAttributes['http.host'], SpanAttributes['url.authority'])) = 'x'", + ) + }) + + // Dependency drilldowns and dashboard templates filter on these keys. The + // spelling the user typed is read first, so a saved legacy filter keeps its + // meaning on spans that dual-emit a different value under the new key. + for (const [key, first, second] of [ + ["db.system", "db.system", "db.system.name"], + ["db.system.name", "db.system.name", "db.system"], + ["messaging.destination.name", "messaging.destination.name", "messaging.destination"], + ["rpc.system", "rpc.system", "rpc.system.name"], + ["rpc.system.name", "rpc.system.name", "rpc.system"], + ] as const) { + it(`matches both semconv spellings when filtering on ${key}`, () => { + const q = tracesListQuery({ attributeFilters: [{ key, value: "x", mode: "equals" }] }) + const { sql } = compileUnsafe(q, baseParams) + expect(sql).toContain( + `if(SpanAttributes['${first}'] != '', SpanAttributes['${first}'], SpanAttributes['${second}']) = 'x'`, + ) + }) + } + it("groups by custom attribute", () => { const q = tracesBreakdownQuery({ metric: "count", diff --git a/packages/query-engine/src/ch/queries/traces.ts b/packages/query-engine/src/ch/queries/traces.ts index 3f0e25caa4..93a676e7da 100644 --- a/packages/query-engine/src/ch/queries/traces.ts +++ b/packages/query-engine/src/ch/queries/traces.ts @@ -18,6 +18,11 @@ import { Traces, TracesAggregatesHourly, } from "../tables" +import { + deploymentEnvExpr, + httpRequestMethodExpr, + httpResponseStatusCodeExpr, +} from "@maple/domain/tinybird/semconv-renames" import { METRIC_NEEDS } from "../../traces-shared" import type { ColumnDefs } from "@maple-dev/effect-clickhouse/types" import * as T from "@maple-dev/effect-clickhouse/types" @@ -172,7 +177,7 @@ function buildGroupNameExpr( parts.push(CH.toString_($.StatusCode)) break case "http_method": - parts.push(CH.toString_($.SpanAttributes.get("http.method"))) + parts.push(CH.toString_(httpRequestMethodExpr($.SpanAttributes))) break case "attribute": if (groupByAttributeKeys?.length) { @@ -289,13 +294,13 @@ function buildBreakdownGroupExpr( case "namespace": return $.ResourceAttributes.get("service.namespace") case "environment": - return $.ResourceAttributes.get("deployment.environment") + return deploymentEnvExpr($.ResourceAttributes) case "span_name": return $.SpanName case "status_code": return $.StatusCode case "http_method": - return $.SpanAttributes.get("http.method") + return httpRequestMethodExpr($.SpanAttributes) case "attribute": return groupByAttributeKey ? $.SpanAttributes.get(groupByAttributeKey) : $.ServiceName default: @@ -1452,9 +1457,9 @@ export function tracesRootListQuery(opts: TracesRootListOpts) { rootSpanKind: $.SpanKind, rootSpanStatusCode: $.StatusCode, rootSpanStatusMessage: $.StatusMessage, - rootHttpMethod: $.SpanAttributes.get("http.method"), + rootHttpMethod: httpRequestMethodExpr($.SpanAttributes), rootHttpRoute: $.SpanAttributes.get("http.route"), - rootHttpStatusCode: $.SpanAttributes.get("http.status_code"), + rootHttpStatusCode: httpResponseStatusCodeExpr($.SpanAttributes), rootSpanAttributes: CH.toJSONString(buildProjectedMapExpr(ROOT_SPAN_ATTR_KEYS, "SpanAttributes")), hasError: CH.if_($.StatusCode.eq("Error"), CH.lit(1), CH.lit(0)), })) @@ -1758,9 +1763,9 @@ export function traceListQuery(opts: TraceListOpts) { rootSpanName: argMin($.SpanName, rootOrder), rootSpanKind: argMin($.SpanKind, rootOrder), rootSpanStatusCode: argMin($.StatusCode, rootOrder), - rootHttpMethod: argMin($.SpanAttributes.get("http.method"), rootOrder), + rootHttpMethod: argMin(httpRequestMethodExpr($.SpanAttributes), rootOrder), rootHttpRoute: argMin($.SpanAttributes.get("http.route"), rootOrder), - rootHttpStatusCode: argMin($.SpanAttributes.get("http.status_code"), rootOrder), + rootHttpStatusCode: argMin(httpResponseStatusCodeExpr($.SpanAttributes), rootOrder), rootSpanAttributes: argMin( CH.toJSONString(buildProjectedMapExpr(ROOT_SPAN_ATTR_KEYS, "SpanAttributes")), rootOrder, diff --git a/packages/query-engine/src/traces-shared.ts b/packages/query-engine/src/traces-shared.ts index 2b814f454b..88319717fe 100644 --- a/packages/query-engine/src/traces-shared.ts +++ b/packages/query-engine/src/traces-shared.ts @@ -42,20 +42,33 @@ import * as T from "@maple-dev/effect-clickhouse/types" // Semconv rename coalescing // -// OpenTelemetry renamed several HTTP span attributes in the stable semconv: -// http.method → http.request.method -// http.status_code → http.response.status_code -// `trace_list_mv` coalesces both spellings when it pre-extracts its columns -// (see materializations.ts), so the quick-filter facet counts cover spans that -// use *either* key. Filters that read the raw `traces` table must coalesce the -// same way — otherwise a facet shows a count while applying it matches zero -// rows (the data carries the new key, the filter looked up the old one). - -const HTTP_SEMCONV_ALIASES: Record = { +// OpenTelemetry renamed several span attributes: +// http.method → http.request.method +// http.status_code → http.response.status_code +// db.system → db.system.name +// messaging.destination → messaging.destination.name +// rpc.system → rpc.system.name +// http.host → server.address (and url.authority, the service map's last fallback) +// A filter on either spelling matches spans that carry either key. The key the +// user typed is read first, so a saved filter on a legacy key keeps matching a +// span that dual-emits a different value under the new key (`db.system=mssql` +// next to `db.system.name=microsoft.sql_server`). HTTP method and status are +// legacy-first under both spellings because that is how `trace_list_mv` +// pre-extracts them, and the facet counts must agree with the filter. + +const SPAN_SEMCONV_ALIASES: Record = { "http.method": ["http.method", "http.request.method"], "http.request.method": ["http.method", "http.request.method"], "http.status_code": ["http.status_code", "http.response.status_code"], "http.response.status_code": ["http.status_code", "http.response.status_code"], + "db.system": ["db.system", "db.system.name"], + "db.system.name": ["db.system.name", "db.system"], + "messaging.destination": ["messaging.destination", "messaging.destination.name"], + "messaging.destination.name": ["messaging.destination.name", "messaging.destination"], + "rpc.system": ["rpc.system", "rpc.system.name"], + "rpc.system.name": ["rpc.system.name", "rpc.system"], + "server.address": ["server.address", "http.host", "url.authority"], + "http.host": ["http.host", "server.address", "url.authority"], } satisfies Record /** @@ -117,7 +130,7 @@ export function buildAttrFilterCondition( // `DeploymentEnv` (resource attributes). const aliasTable = mapName === "SpanAttributes" - ? HTTP_SEMCONV_ALIASES + ? SPAN_SEMCONV_ALIASES : mapName === "ResourceAttributes" ? RESOURCE_SEMCONV_ALIASES : undefined diff --git a/packages/ui/src/lib/error-prompt.ts b/packages/ui/src/lib/error-prompt.ts index cbff4c7906..0708ccd2e8 100644 --- a/packages/ui/src/lib/error-prompt.ts +++ b/packages/ui/src/lib/error-prompt.ts @@ -10,19 +10,27 @@ export interface ErrorPromptInput { attributes?: Record } +// Current semconv key first, its deprecated spelling after, so spans from +// either generation of instrumentation keep their context. const RELEVANT_KEYS = [ + "http.request.method", "http.method", + "url.full", "http.url", "http.route", + "http.response.status_code", "http.status_code", "db.system.name", "db.system", "db.query.text", "db.statement", + "rpc.system.name", "rpc.method", "rpc.service", "messaging.system", + "messaging.operation.type", "messaging.operation", + "messaging.destination.name", ] export function formatErrorPrompt({ message, serviceName, operation, attributes }: ErrorPromptInput): string { diff --git a/packages/ui/src/lib/log-attributes.test.ts b/packages/ui/src/lib/log-attributes.test.ts new file mode 100644 index 0000000000..6db024a3b1 --- /dev/null +++ b/packages/ui/src/lib/log-attributes.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "vitest" +import { getChipTone, pickImportantAttributes } from "./log-attributes" + +describe("getChipTone for rpc.response.status_code", () => { + it("marks the gRPC codes semconv treats as server errors", () => { + expect(getChipTone("rpc.response.status_code", "UNAVAILABLE", "INFO", "grpc")).toBe("error") + expect(getChipTone("rpc.response.status_code", "internal", "INFO", "grpc")).toBe("error") + expect(getChipTone("rpc.response.status_code", "14", "INFO", "grpc")).toBe("error") + }) + + it("warns on other non-OK gRPC codes and leaves OK alone", () => { + expect(getChipTone("rpc.response.status_code", "NOT_FOUND", "INFO", "grpc")).toBe("warn") + expect(getChipTone("rpc.response.status_code", "OK", "INFO", "grpc")).toBe("muted") + expect(getChipTone("rpc.response.status_code", "0", "INFO", "grpc")).toBe("muted") + }) + + it("keeps values from other or unknown rpc systems neutral", () => { + expect(getChipTone("rpc.response.status_code", "-32601", "INFO", "grpc")).toBe("muted") + expect(getChipTone("rpc.response.status_code", "INTERNAL", "INFO", "jsonrpc")).toBe("muted") + expect(getChipTone("rpc.response.status_code", "2", "INFO")).toBe("muted") + }) + + it("reads the rpc system from the row for the inline chips", () => { + const log = { + logAttributes: { "rpc.system.name": "grpc", "rpc.response.status_code": "UNAVAILABLE" }, + resourceAttributes: {}, + serviceName: "api", + severityText: "INFO", + } + const status = pickImportantAttributes(log).find((a) => a.key === "rpc.response.status_code") + expect(status?.tone).toBe("error") + }) +}) diff --git a/packages/ui/src/lib/log-attributes.ts b/packages/ui/src/lib/log-attributes.ts index 46bbe28a62..1f40325efb 100644 --- a/packages/ui/src/lib/log-attributes.ts +++ b/packages/ui/src/lib/log-attributes.ts @@ -49,7 +49,7 @@ const PROMOTED_RESOURCE_KEYS = new Set([ function scoreKey(key: string): number { if (key === "error" || key === "exception" || key.startsWith("exception.")) return 100 if (key === "http.status_code" || key === "http.response.status_code") return 95 - if (key === "rpc.grpc.status_code") return 90 + if (key === "rpc.grpc.status_code" || key === "rpc.response.status_code") return 90 if (key === "http.method" || key === "http.request.method") return 80 if ( key === "db.system" || @@ -60,10 +60,10 @@ function scoreKey(key: string): number { key === "db.operation.name" ) return 70 - if (key === "rpc.service" || key === "rpc.method") return 68 + if (key === "rpc.system.name" || key === "rpc.service" || key === "rpc.method") return 68 if (key === "user.id" || key === "enduser.id" || key === "customer_id" || key === "customer.id") return 66 if (key === "duration_ms" || key === "latency_ms" || key === "http.duration") return 60 - if (key === "http.url" || key === "http.route" || key === "url.path") return 55 + if (key === "http.url" || key === "url.full" || key === "http.route" || key === "url.path") return 55 if (key.startsWith("http.") || key.startsWith("url.")) return 40 if (key.startsWith("db.")) return 38 if (key.startsWith("rpc.")) return 36 @@ -73,12 +73,43 @@ function scoreKey(key: string): number { return 20 } +// `rpc.response.status_code` values are system-specific, so they are only toned +// when the row says the system is gRPC. gRPC's are status names (or their +// numbers from older instrumentation); semconv counts only these as errors on a +// server span, and the chip does not know the span kind, so the other non-OK +// codes are a warning. +const GRPC_SERVER_ERROR_CODES = new Set([ + "UNKNOWN", + "DEADLINE_EXCEEDED", + "UNIMPLEMENTED", + "INTERNAL", + "UNAVAILABLE", + "DATA_LOSS", + "2", + "4", + "12", + "13", + "14", + "15", +]) +const GRPC_CODES = new Set([ + ...GRPC_SERVER_ERROR_CODES, + ..."OK CANCELLED INVALID_ARGUMENT NOT_FOUND ALREADY_EXISTS PERMISSION_DENIED RESOURCE_EXHAUSTED FAILED_PRECONDITION ABORTED OUT_OF_RANGE UNAUTHENTICATED".split( + " ", + ), + ..."0 1 3 5 6 7 8 9 10 11 16".split(" "), +]) + function isNumericStatus(value: string): number | null { const n = Number(value) return Number.isInteger(n) && n >= 100 && n < 600 ? n : null } -export function getChipTone(key: string, value: string, severityText: string): ChipTone { +/** + * `rpcSystem` is the row's `rpc.system.name` (or legacy `rpc.system`); without + * it `rpc.response.status_code` stays neutral. + */ +export function getChipTone(key: string, value: string, severityText: string, rpcSystem?: string): ChipTone { const sev = severityText.toUpperCase() const rowIsError = sev === "ERROR" || sev === "FATAL" @@ -104,8 +135,20 @@ export function getChipTone(key: string, value: string, severityText: string): C if (Number.isFinite(n) && n !== 0) return "error" } + if (key === "rpc.response.status_code" && rpcSystem?.toLowerCase() === "grpc") { + const code = value.toUpperCase() + if (GRPC_SERVER_ERROR_CODES.has(code)) return "error" + if (GRPC_CODES.has(code) && code !== "OK" && code !== "0") return "warn" + } + if (key === "http.method" || key === "http.request.method") return "info" - if (key === "db.system" || key === "db.system.name" || key === "rpc.service" || key === "rpc.method") + if ( + key === "db.system" || + key === "db.system.name" || + key === "rpc.system.name" || + key === "rpc.service" || + key === "rpc.method" + ) return "info" if (rowIsError) return "muted" @@ -118,6 +161,7 @@ function shouldSkip(key: string): boolean { } export function pickImportantAttributes(log: LogLike, limit = 4): PickedAttribute[] { + const rpcSystem = log.logAttributes["rpc.system.name"] || log.logAttributes["rpc.system"] const serviceNameLower = log.serviceName.toLowerCase() const scored: Array<{ key: string; value: string; score: number; source: "log" | "resource" }> = [] @@ -141,7 +185,7 @@ export function pickImportantAttributes(log: LogLike, limit = 4): PickedAttribut return scored.slice(0, limit).map(({ key, value, source }) => ({ key, value, - tone: getChipTone(key, value, log.severityText), + tone: getChipTone(key, value, log.severityText, rpcSystem), source, })) }