From 181a72dc3a8e68d9e6e5d268d1fda33ed537d954 Mon Sep 17 00:00:00 2001 From: JeremyFunk Date: Mon, 28 Sep 2026 09:43:35 +0200 Subject: [PATCH 1/7] perf(traces): add trace_facets_hourly rollup for the sidebar facets The traces sidebar facets (tracesFacets) run seven UNION branches, each a full scan of trace_list_mv over the page window. A busy org writes ~14M root spans a day there, so past about a day the query exceeds the 5s discovery budget and the sidebar fails (code 159). A single-pass rewrite measured only ~2x faster on prod data; a 3-day window still took 6.7s. trace_facets_hourly rolls trace_list_mv up hourly by the facet dimensions (service, span name, HTTP method/status, environment, namespace, error flag) with root-span counts and duration min/max/t-digest state, so the same answer is a read of a few hundred rows per org-hour (78 for our own org, against ~590k root spans). The view is cascaded off trace_list_mv, so its values are the ones the trace list filters on, with no fourth copy of the span-name rewrite. Migration 0034 (local schema v24) creates it, backfills the 30 days trace_list_mv retains with the view detached, then attaches the view. Schema only: nothing reads the table yet. The read path follows in a separate PR once the Tinybird populate has finished. --- apps/cli/src/server/local-schema-history.ts | 13 + apps/cli/src/server/local-schema-version.ts | 2 +- .../server/local-store-migrations/steps.ts | 28 + apps/cli/src/server/schema-identity.ts | 3 + apps/cli/src/server/schema/local-inserts.json | 2 +- .../src/server/schema/local-schema-v24.sql | 2135 +++++++++++++++++ apps/cli/src/server/schema/local-schema.sql | 42 +- apps/cli/test/local-store-migrations.test.ts | 22 +- apps/cli/test/native-local-store-migration.sh | 2 +- apps/ingest/src/clickhouse_insert_mappings.rs | 2 +- docs/warehouse-rollups.md | 2 +- ...rly-materialization.clickhouse.e2e.test.ts | 204 ++ .../services/warehouse/warehouse-catalog.ts | 5 + packages/domain/src/clickhouse/backfill.ts | 1 + .../migrations/0034_trace_facets_hourly.ts | 79 + .../src/clickhouse/migrations/index.test.ts | 31 +- .../domain/src/clickhouse/migrations/index.ts | 2 + packages/domain/src/clickhouse/qualify.ts | 1 + .../domain/src/generated/clickhouse-schema.ts | 4 +- .../generated/tinybird-project-manifest.ts | 12 +- packages/domain/src/tinybird/datasources.ts | 53 + .../domain/src/tinybird/materializations.ts | 36 + .../materialized-projection-order.test.ts | 1 + .../src/tinybird/retention-matrix.test.ts | 3 + 24 files changed, 2666 insertions(+), 19 deletions(-) create mode 100644 apps/cli/src/server/schema/local-schema-v24.sql create mode 100644 packages/backend/src/services/warehouse/trace-facets-hourly-materialization.clickhouse.e2e.test.ts create mode 100644 packages/domain/src/clickhouse/migrations/0034_trace_facets_hourly.ts diff --git a/apps/cli/src/server/local-schema-history.ts b/apps/cli/src/server/local-schema-history.ts index 8dd815b084..34988ba39a 100644 --- a/apps/cli/src/server/local-schema-history.ts +++ b/apps/cli/src/server/local-schema-history.ts @@ -288,6 +288,19 @@ export const LOCAL_SCHEMA_HISTORY: ReadonlyArray = Obje manifestDigest: "eac1c292f9f3d9d799d4a76a7a9991c258aaacf344d31479ebb5bf6014a6840a", projectRevision: "ed74788ef292834069e0ea6ee3b22d68fc604fb66cb54d2d551db67ce8d20b3a", }), + Object.freeze({ + // v24 adds trace_facets_hourly and its view off trace_list_mv. Nothing is + // rewritten and no row moves; the rollup is not backfilled locally. + // + // projectRevision is carried forward deliberately: it is a hardcoded + // constant that no longer tracks the generator's header, and the identity + // this gate compares is the fingerprint/digest pair. + version: 24, + fingerprint: "b59c68cd72efa658", + digest: "b59c68cd72efa658d9c5cf602aac7b6cba32947db3d939c03f407c03865be778", + manifestDigest: "a3fc89a7c2ca86dc558f8e5c3f59fdb0bfbbcbe12cb4dce2a9cd8637ae133aef", + projectRevision: "ed74788ef292834069e0ea6ee3b22d68fc604fb66cb54d2d551db67ce8d20b3a", + }), ] as const) /** Immutable SQLite control DDL identities, checked by clickhouse:schema:check. */ diff --git a/apps/cli/src/server/local-schema-version.ts b/apps/cli/src/server/local-schema-version.ts index bfd262b514..53b093eee1 100644 --- a/apps/cli/src/server/local-schema-version.ts +++ b/apps/cli/src/server/local-schema-version.ts @@ -1,7 +1,7 @@ // Increment this value for every structural change to the generated local // schema. The compatibility manifest and migration registry must be updated in // the same change before a new value can ship. -export const LOCAL_SCHEMA_VERSION = 23 as const +export const LOCAL_SCHEMA_VERSION = 24 as const /** SQLite eventing state has its own independent version sequence. */ export const LOCAL_CONTROL_SCHEMA_VERSION = 1 as const diff --git a/apps/cli/src/server/local-store-migrations/steps.ts b/apps/cli/src/server/local-store-migrations/steps.ts index 6c5aae294d..a6b391cb27 100644 --- a/apps/cli/src/server/local-store-migrations/steps.ts +++ b/apps/cli/src/server/local-store-migrations/steps.ts @@ -1052,5 +1052,33 @@ export const LOCAL_STORE_STEPS: ReadonlyArray = [ }, ], }, + { + // Both objects are new, so the bootstrap's IF NOT EXISTS CREATEs are the whole edge. + id: "local-0023-to-0024-trace-facets-hourly", + from: 23, + to: 24, + description: + "Create trace_facets_hourly and its materialized view so the traces sidebar facets read an hourly rollup instead of scanning trace_list_mv", + clonedBefore: "any DDL runs", + plan: [ + [ + "create-trace-facets-hourly", + "Create trace_facets_hourly and trace_facets_hourly_mv via the v24 bootstrap (both new, IF NOT EXISTS)", + ], + ], + verifies: "Verify the v24 physical schema and the retained raw telemetry counts", + dispositions: [ + { + name: "trace_facets_hourly", + classification: "derived", + disposition: "rebuild-within-retention-horizon", + guarantee: + "The rollup accrues for root spans ingested after the migration; older ones stay in trace_list_mv until they age out.", + preservationInterval: "from the migration forward", + sourceRetentionDays: 30, + targetRetentionDays: 30, + }, + ], + }, // local-schema:bump appends the next step above this line. ] diff --git a/apps/cli/src/server/schema-identity.ts b/apps/cli/src/server/schema-identity.ts index d62bcfbe59..f20f9bdc92 100644 --- a/apps/cli/src/server/schema-identity.ts +++ b/apps/cli/src/server/schema-identity.ts @@ -22,6 +22,7 @@ import schemaV20Sql from "./schema/local-schema-v20.sql" with { type: "text" } import schemaV21Sql from "./schema/local-schema-v21.sql" with { type: "text" } import schemaV22Sql from "./schema/local-schema-v22.sql" with { type: "text" } import schemaV23Sql from "./schema/local-schema-v23.sql" with { type: "text" } +import schemaV24Sql from "./schema/local-schema-v24.sql" with { type: "text" } import { schemaDigest as digestSchema, schemaFingerprint as fingerprintSchema } from "./store-version" import { buildLocalSchemaManifest, type LocalSchemaManifest } from "./schema-manifest" import { LOCAL_SCHEMA_VERSION } from "./local-schema-version" @@ -89,6 +90,7 @@ const SNAPSHOT_SQL: ReadonlyArray = [ schemaV21Sql, schemaV22Sql, schemaV23Sql, + schemaV24Sql, ] export interface LocalSchemaSnapshot { @@ -169,6 +171,7 @@ export const LOCAL_SCHEMA_V20 = localSchemaIdentity(20) export const LOCAL_SCHEMA_V21 = localSchemaIdentity(21) export const LOCAL_SCHEMA_V22 = localSchemaIdentity(22) export const LOCAL_SCHEMA_V23 = localSchemaIdentity(23) +export const LOCAL_SCHEMA_V24 = localSchemaIdentity(24) export const CURRENT_LOCAL_SCHEMA: LocalSchemaIdentity = Object.freeze({ version: LOCAL_SCHEMA_VERSION, diff --git a/apps/cli/src/server/schema/local-inserts.json b/apps/cli/src/server/schema/local-inserts.json index b48940ec68..1ee28c0dc9 100644 --- a/apps/cli/src/server/schema/local-inserts.json +++ b/apps/cli/src/server/schema/local-inserts.json @@ -1,5 +1,5 @@ { - "projectRevision": "455ff9a387c6e0b10a3083278c020d73ea622f1e30a106fb833bcb2262a94d6f", + "projectRevision": "eee23295aa263ed5dce623418acbe3cdffac84bbea15570e3ac20b4e1d3b9b50", "orgPlaceholder": "__ORG__", "datasources": { "traces": { diff --git a/apps/cli/src/server/schema/local-schema-v24.sql b/apps/cli/src/server/schema/local-schema-v24.sql new file mode 100644 index 0000000000..260927ae10 --- /dev/null +++ b/apps/cli/src/server/schema/local-schema-v24.sql @@ -0,0 +1,2135 @@ +-- This file is generated by scripts/generate-clickhouse-schema-sql.ts +-- Do not edit manually. Run `bun run clickhouse:schema` to regenerate. +-- projectRevision: eee23295aa263ed5dce623418acbe3cdffac84bbea15570e3ac20b4e1d3b9b50 +-- localSchemaVersion: 23 + +CREATE TABLE IF NOT EXISTS ai_crawler_requests ( + OrgId LowCardinality(String), + Timestamp DateTime64(9), + TraceId String, + ServiceName LowCardinality(String), + Crawler LowCardinality(String), + Host LowCardinality(String), + Path String, + HttpStatus UInt16 +) +ENGINE = MergeTree +PARTITION BY toDate(Timestamp) +ORDER BY (OrgId, Timestamp, TraceId) +TTL toDate(Timestamp) + INTERVAL 30 DAY; + +CREATE TABLE IF NOT EXISTS ai_trace_index ( + OrgId LowCardinality(String), + Timestamp DateTime64(9), + TraceId String, + SessionId String, + VendorId LowCardinality(String), + ServiceName LowCardinality(String), + DeploymentEnv LowCardinality(String), + Model LowCardinality(String), + AgentName LowCardinality(String), + ToolName LowCardinality(String), + SpanId String, + ParentSpanId String, + Duration UInt64, + IsError UInt8, + IsLlmCall UInt8, + IsToolCall UInt8, + Tokens Float64, + Cost Float64, + ResponseId String, + VendorVersion LowCardinality(String), + InputTokens Float64, + CacheReadTokens Float64, + CacheWriteTokens Float64, + OutputTokens Float64, + ReasoningTokens Float64, + ErrorType LowCardinality(String), + StatusMessage String, + ToolDescription String, + FailedToolCallResult String, + ErrorFingerprint UInt64 +) +ENGINE = MergeTree +PARTITION BY toDate(Timestamp) +ORDER BY (OrgId, Timestamp, TraceId) +TTL toDate(Timestamp) + INTERVAL 30 DAY; + +CREATE TABLE IF NOT EXISTS alert_checks ( + OrgId LowCardinality(String), + RuleId String, + GroupKey String, + Timestamp DateTime64(3), + Status LowCardinality(String), + SignalType LowCardinality(String), + Comparator LowCardinality(String), + Threshold Float64, + ObservedValue Nullable(Float64), + SampleCount UInt32, + WindowMinutes UInt16, + WindowStart DateTime64(3), + WindowEnd DateTime64(3), + ConsecutiveBreaches UInt16, + ConsecutiveHealthy UInt16, + IncidentId Nullable(String), + IncidentTransition LowCardinality(String), + EvaluationDurationMs UInt32, + ErrorMessage Nullable(String), + ErrorCategory LowCardinality(String) +) +ENGINE = MergeTree +PARTITION BY toDate(Timestamp) +ORDER BY (OrgId, RuleId, GroupKey, Timestamp) +TTL toDate(Timestamp) + INTERVAL 365 DAY; + +CREATE TABLE IF NOT EXISTS attribute_keys_hourly ( + OrgId LowCardinality(String), + Hour DateTime, + AttributeKey LowCardinality(String), + AttributeScope LowCardinality(String), + UsageCount SimpleAggregateFunction(sum, UInt64) +) +ENGINE = AggregatingMergeTree +PARTITION BY toDate(Hour) +ORDER BY (OrgId, AttributeScope, Hour, AttributeKey) +TTL Hour + INTERVAL 90 DAY; + +CREATE TABLE IF NOT EXISTS attribute_values_hourly ( + OrgId LowCardinality(String), + Hour DateTime, + AttributeKey LowCardinality(String), + AttributeValue String, + AttributeScope LowCardinality(String), + UsageCount SimpleAggregateFunction(sum, UInt64) +) +ENGINE = AggregatingMergeTree +PARTITION BY toDate(Hour) +ORDER BY (OrgId, AttributeScope, AttributeKey, Hour, AttributeValue) +TTL Hour + INTERVAL 90 DAY; + +CREATE TABLE IF NOT EXISTS audit_log ( + OrgId LowCardinality(String), + Id String, + OccurredAt DateTime64(3), + RecordedAt DateTime64(3), + ActorType LowCardinality(String), + UserId String, + ApiKeyId String, + ActorId String, + ActorLabel String, + AffectedUserId String, + Source LowCardinality(String), + Action LowCardinality(String), + Outcome LowCardinality(String), + DenialReason String, + ResourceType LowCardinality(String), + ResourceId String, + ChangedFields Array(String), + Changes String, + Metadata String, + RequestId String, + OriginIp String, + OriginCountry LowCardinality(String) +) +ENGINE = ReplacingMergeTree +PARTITION BY toYYYYMM(OccurredAt) +ORDER BY (OrgId, OccurredAt, Id) +TTL toDate(OccurredAt) + INTERVAL 2190 DAY; + +CREATE TABLE IF NOT EXISTS error_events ( + OrgId LowCardinality(String), + Timestamp DateTime, + TraceId String, + SpanId String, + ParentSpanId String DEFAULT '__unset__', + ServiceName LowCardinality(String), + DeploymentEnv LowCardinality(String), + ExceptionType LowCardinality(String), + ExceptionMessage String, + ExceptionStacktrace String, + TopFrame String, + FingerprintHash UInt64, + StatusMessage String, + Duration UInt64, + ErrorLabel String, + ServiceVersion LowCardinality(String) +) +ENGINE = MergeTree +PARTITION BY toDate(Timestamp) +ORDER BY (OrgId, FingerprintHash, Timestamp) +TTL Timestamp + INTERVAL 90 DAY; + +CREATE TABLE IF NOT EXISTS error_events_by_time ( + OrgId LowCardinality(String), + Timestamp DateTime, + TraceId String, + SpanId String, + ParentSpanId String DEFAULT '__unset__', + ServiceName LowCardinality(String), + DeploymentEnv LowCardinality(String), + ExceptionType LowCardinality(String), + ExceptionMessage String, + ExceptionStacktrace String, + TopFrame String, + FingerprintHash UInt64, + StatusMessage String, + Duration UInt64, + ErrorLabel String, + ServiceVersion LowCardinality(String) +) +ENGINE = MergeTree +PARTITION BY toDate(Timestamp) +ORDER BY (OrgId, Timestamp, FingerprintHash) +TTL Timestamp + INTERVAL 90 DAY; + +CREATE TABLE IF NOT EXISTS error_fingerprints_minutely ( + OrgId LowCardinality(String), + Minute DateTime, + FingerprintHash UInt64, + ServiceName SimpleAggregateFunction(anyLast, String), + ExceptionType SimpleAggregateFunction(anyLast, String), + ExceptionMessage SimpleAggregateFunction(anyLast, String), + ErrorLabel SimpleAggregateFunction(anyLast, String), + TopFrame SimpleAggregateFunction(anyLast, String), + OccurrenceCount SimpleAggregateFunction(sum, UInt64), + FirstSeen SimpleAggregateFunction(min, DateTime), + LastSeen SimpleAggregateFunction(max, DateTime), + ServiceVersions SimpleAggregateFunction(groupUniqArrayArray, Array(String)) +) +ENGINE = AggregatingMergeTree +PARTITION BY toYYYYMM(Minute) +ORDER BY (OrgId, Minute, FingerprintHash) +TTL Minute + INTERVAL 90 DAY; + +CREATE TABLE IF NOT EXISTS identity_links ( + OrgId LowCardinality(String), + VisitorId String, + UserId String, + FirstSeen SimpleAggregateFunction(min, DateTime64(9)) +) +ENGINE = AggregatingMergeTree +PARTITION BY tuple() +ORDER BY (OrgId, VisitorId, UserId) +TTL toDate(FirstSeen) + INTERVAL 365 DAY; + +CREATE TABLE IF NOT EXISTS logs ( + OrgId LowCardinality(String), + Timestamp DateTime64(9), + TimestampTime DateTime, + TraceId String, + SpanId String, + TraceFlags UInt8, + SeverityText LowCardinality(String), + SeverityNumber UInt8, + ServiceName LowCardinality(String), + Body String, + ResourceSchemaUrl String, + ResourceAttributes Map(LowCardinality(String), String), + ScopeSchemaUrl String, + ScopeName String, + ScopeVersion String, + ScopeAttributes Map(LowCardinality(String), String), + LogAttributes Map(LowCardinality(String), String), + ResourceAttributeItems Array(String) DEFAULT arrayMap((k, v) -> concat(k, char(31), v), mapKeys(ResourceAttributes), mapValues(ResourceAttributes)), + ScopeAttributeItems Array(String) DEFAULT arrayMap((k, v) -> concat(k, char(31), v), mapKeys(ScopeAttributes), mapValues(ScopeAttributes)), + LogAttributeItems Array(String) DEFAULT arrayMap((k, v) -> concat(k, char(31), v), mapKeys(LogAttributes), mapValues(LogAttributes)), + INDEX idx_trace_id TraceId TYPE bloom_filter(0.01) GRANULARITY 1, + INDEX idx_resource_attr_keys mapKeys(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1, + INDEX idx_resource_attr_vals mapValues(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1, + INDEX idx_scope_attr_keys mapKeys(ScopeAttributes) TYPE bloom_filter(0.01) GRANULARITY 1, + INDEX idx_scope_attr_vals mapValues(ScopeAttributes) TYPE bloom_filter(0.01) GRANULARITY 1, + INDEX idx_log_attr_keys mapKeys(LogAttributes) TYPE bloom_filter(0.01) GRANULARITY 1, + INDEX idx_log_attr_vals mapValues(LogAttributes) TYPE bloom_filter(0.01) GRANULARITY 1, + INDEX idx_lower_body lower(Body) TYPE tokenbf_v1(32768, 3, 0) GRANULARITY 8 +) +ENGINE = MergeTree +PARTITION BY toDate(TimestampTime) +ORDER BY (OrgId, toStartOfFiveMinutes(Timestamp), ServiceName, Timestamp) +TTL toDate(TimestampTime) + INTERVAL 30 DAY; + +CREATE TABLE IF NOT EXISTS logs_aggregates_hourly ( + OrgId LowCardinality(String), + Hour DateTime, + ServiceName LowCardinality(String), + SeverityText LowCardinality(String), + DeploymentEnv LowCardinality(String), + Count SimpleAggregateFunction(sum, UInt64), + SizeBytes SimpleAggregateFunction(sum, UInt64), + ServiceNamespace LowCardinality(String), + INDEX idx_service_namespace ServiceNamespace TYPE set(1000) GRANULARITY 4 +) +ENGINE = AggregatingMergeTree +PARTITION BY toDate(Hour) +ORDER BY (OrgId, Hour, ServiceName, SeverityText, DeploymentEnv, ServiceNamespace) +TTL toDate(Hour) + INTERVAL 365 DAY; + +CREATE TABLE IF NOT EXISTS metric_catalog ( + OrgId LowCardinality(String), + Hour DateTime, + MetricType LowCardinality(String), + ServiceName LowCardinality(String), + MetricName LowCardinality(String), + MetricDescription SimpleAggregateFunction(anyLast, String), + MetricUnit SimpleAggregateFunction(anyLast, String), + IsMonotonic SimpleAggregateFunction(anyLast, UInt8), + DataPointCount SimpleAggregateFunction(sum, UInt64), + FirstSeen SimpleAggregateFunction(min, DateTime), + LastSeen SimpleAggregateFunction(max, DateTime) +) +ENGINE = AggregatingMergeTree +PARTITION BY toDate(Hour) +ORDER BY (OrgId, MetricType, ServiceName, MetricName, Hour) +TTL Hour + INTERVAL 90 DAY; + +CREATE TABLE IF NOT EXISTS metrics_exponential_histogram ( + OrgId LowCardinality(String), + ResourceAttributes Map(LowCardinality(String), String), + ResourceSchemaUrl String, + ScopeName String, + ScopeVersion String, + ScopeAttributes Map(LowCardinality(String), String), + ScopeSchemaUrl String, + ServiceName LowCardinality(String), + MetricName LowCardinality(String), + MetricDescription LowCardinality(String), + MetricUnit LowCardinality(String), + Attributes Map(LowCardinality(String), String), + StartTimeUnix DateTime64(9), + TimeUnix DateTime64(9), + Count UInt64, + Sum Float64, + Scale Int32, + ZeroCount UInt64, + PositiveOffset Int32, + PositiveBucketCounts Array(UInt64), + NegativeOffset Int32, + NegativeBucketCounts Array(UInt64), + ExemplarsTraceId Array(String), + ExemplarsSpanId Array(String), + ExemplarsTimestamp Array(DateTime64(9)), + ExemplarsValue Array(Float64), + ExemplarsFilteredAttributes Array(Map(LowCardinality(String), String)), + Flags UInt32, + Min Nullable(Float64), + Max Nullable(Float64), + AggregationTemporality Int32 +) +ENGINE = MergeTree +PARTITION BY toDate(TimeUnix) +ORDER BY (OrgId, ServiceName, MetricName, Attributes, toUnixTimestamp64Nano(TimeUnix)) +TTL toDate(TimeUnix) + INTERVAL 90 DAY; + +CREATE TABLE IF NOT EXISTS metrics_gauge ( + OrgId LowCardinality(String), + ResourceAttributes Map(LowCardinality(String), String), + ResourceSchemaUrl String, + ScopeName String, + ScopeVersion String, + ScopeAttributes Map(LowCardinality(String), String), + ScopeSchemaUrl String, + ServiceName LowCardinality(String), + MetricName LowCardinality(String), + MetricDescription LowCardinality(String), + MetricUnit LowCardinality(String), + Attributes Map(LowCardinality(String), String), + StartTimeUnix DateTime64(9), + TimeUnix DateTime64(9), + Value Float64, + Flags UInt32, + ExemplarsTraceId Array(String), + ExemplarsSpanId Array(String), + ExemplarsTimestamp Array(DateTime64(9)), + ExemplarsValue Array(Float64), + ExemplarsFilteredAttributes Array(Map(LowCardinality(String), String)) +) +ENGINE = MergeTree +PARTITION BY toDate(TimeUnix) +ORDER BY (OrgId, ServiceName, MetricName, Attributes, toUnixTimestamp64Nano(TimeUnix)) +TTL toDate(TimeUnix) + INTERVAL 90 DAY; + +CREATE TABLE IF NOT EXISTS metrics_histogram ( + OrgId LowCardinality(String), + ResourceAttributes Map(LowCardinality(String), String), + ResourceSchemaUrl String, + ScopeName String, + ScopeVersion String, + ScopeAttributes Map(LowCardinality(String), String), + ScopeSchemaUrl String, + ServiceName LowCardinality(String), + MetricName LowCardinality(String), + MetricDescription LowCardinality(String), + MetricUnit LowCardinality(String), + Attributes Map(LowCardinality(String), String), + StartTimeUnix DateTime64(9), + TimeUnix DateTime64(9), + Count UInt64, + Sum Float64, + BucketCounts Array(UInt64), + ExplicitBounds Array(Float64), + ExemplarsTraceId Array(String), + ExemplarsSpanId Array(String), + ExemplarsTimestamp Array(DateTime64(9)), + ExemplarsValue Array(Float64), + ExemplarsFilteredAttributes Array(Map(LowCardinality(String), String)), + Flags UInt32, + Min Nullable(Float64), + Max Nullable(Float64), + AggregationTemporality Int32 +) +ENGINE = MergeTree +PARTITION BY toDate(TimeUnix) +ORDER BY (OrgId, ServiceName, MetricName, Attributes, toUnixTimestamp64Nano(TimeUnix)) +TTL toDate(TimeUnix) + INTERVAL 90 DAY; + +CREATE TABLE IF NOT EXISTS metrics_sum ( + OrgId LowCardinality(String), + ResourceAttributes Map(LowCardinality(String), String), + ResourceSchemaUrl String, + ScopeName String, + ScopeVersion String, + ScopeAttributes Map(LowCardinality(String), String), + ScopeSchemaUrl String, + ServiceName LowCardinality(String), + MetricName LowCardinality(String), + MetricDescription LowCardinality(String), + MetricUnit LowCardinality(String), + Attributes Map(LowCardinality(String), String), + StartTimeUnix DateTime64(9), + TimeUnix DateTime64(9), + Value Float64, + Flags UInt32, + ExemplarsTraceId Array(String), + ExemplarsSpanId Array(String), + ExemplarsTimestamp Array(DateTime64(9)), + ExemplarsValue Array(Float64), + ExemplarsFilteredAttributes Array(Map(LowCardinality(String), String)), + AggregationTemporality Int32, + IsMonotonic Bool +) +ENGINE = MergeTree +PARTITION BY toDate(TimeUnix) +ORDER BY (OrgId, ServiceName, MetricName, Attributes, toUnixTimestamp64Nano(TimeUnix)) +TTL toDate(TimeUnix) + INTERVAL 90 DAY; + +CREATE TABLE IF NOT EXISTS product_events ( + OrgId LowCardinality(String), + Timestamp DateTime64(9), + Source LowCardinality(String) DEFAULT 'browser', + SessionId String DEFAULT '', + Seq UInt32 DEFAULT 0, + VisitorId String DEFAULT '', + UserId String DEFAULT '', + GroupId String DEFAULT '', + Kind LowCardinality(String), + EventName String, + Host LowCardinality(String) DEFAULT '', + PagePath String DEFAULT '', + Url String DEFAULT '', + ServiceName LowCardinality(String) DEFAULT '', + Attributes Map(String, String) DEFAULT map(), + TraceId String DEFAULT '', + SpanId String DEFAULT '', + INDEX idx_event_name EventName TYPE set(64) GRANULARITY 4, + INDEX idx_user_id UserId TYPE bloom_filter GRANULARITY 4, + INDEX idx_trace_id TraceId TYPE bloom_filter GRANULARITY 4 +) +ENGINE = MergeTree +PARTITION BY toDate(Timestamp) +ORDER BY (OrgId, Timestamp, VisitorId, SessionId, Seq) +TTL toDate(Timestamp) + INTERVAL 365 DAY; + +CREATE TABLE IF NOT EXISTS service_address_resolutions_hourly ( + OrgId LowCardinality(String), + Hour DateTime, + SourceService LowCardinality(String), + ParentServerAddress String, + ResolvedTargetService LowCardinality(String), + DeploymentEnv LowCardinality(String) +) +ENGINE = ReplacingMergeTree +PARTITION BY toDate(Hour) +ORDER BY (OrgId, Hour, DeploymentEnv, SourceService, ParentServerAddress, ResolvedTargetService) +TTL toDate(Hour) + INTERVAL 365 DAY; + +CREATE TABLE IF NOT EXISTS service_external_edges_hourly ( + OrgId LowCardinality(String), + Hour DateTime, + ServiceName LowCardinality(String), + TargetType LowCardinality(String), + TargetSystem LowCardinality(String), + TargetName String, + DeploymentEnv LowCardinality(String), + CallCount SimpleAggregateFunction(sum, UInt64), + ErrorCount SimpleAggregateFunction(sum, UInt64), + DurationSumMs SimpleAggregateFunction(sum, Float64), + MaxDurationMs SimpleAggregateFunction(max, Float64), + SampleRateSum SimpleAggregateFunction(sum, Float64), + DurationQuantiles AggregateFunction(quantilesTDigestWeighted(0.5, 0.95), UInt64, UInt32) +) +ENGINE = AggregatingMergeTree +PARTITION BY toDate(Hour) +ORDER BY (OrgId, Hour, DeploymentEnv, ServiceName, TargetType, TargetSystem, TargetName) +TTL toDate(Hour) + INTERVAL 365 DAY; + +CREATE TABLE IF NOT EXISTS service_map_children ( + OrgId LowCardinality(String), + Timestamp DateTime, + TraceId String, + ParentSpanId String, + ServiceName LowCardinality(String), + SpanKind LowCardinality(String), + Duration UInt64, + StatusCode LowCardinality(String), + TraceState String, + DeploymentEnv LowCardinality(String) +) +ENGINE = MergeTree +PARTITION BY toDate(Timestamp) +ORDER BY (OrgId, TraceId, ParentSpanId, Timestamp) +TTL Timestamp + INTERVAL 30 DAY; + +CREATE TABLE IF NOT EXISTS service_map_db_edges_hourly ( + OrgId LowCardinality(String), + Hour DateTime, + ServiceName LowCardinality(String), + DbSystem LowCardinality(String), + DeploymentEnv LowCardinality(String), + CallCount SimpleAggregateFunction(sum, UInt64), + ErrorCount SimpleAggregateFunction(sum, UInt64), + DurationSumMs SimpleAggregateFunction(sum, Float64), + MaxDurationMs SimpleAggregateFunction(max, Float64), + SampledSpanCount SimpleAggregateFunction(sum, UInt64), + UnsampledSpanCount SimpleAggregateFunction(sum, UInt64), + SampleRateSum SimpleAggregateFunction(sum, Float64), + DbNamespace LowCardinality(String), + DurationQuantiles AggregateFunction(quantilesTDigestWeighted(0.5, 0.95), UInt64, UInt32) +) +ENGINE = AggregatingMergeTree +PARTITION BY toDate(Hour) +ORDER BY (OrgId, Hour, DeploymentEnv, ServiceName, DbSystem, DbNamespace) +TTL toDate(Hour) + INTERVAL 365 DAY; + +CREATE TABLE IF NOT EXISTS service_map_db_query_shapes_hourly ( + OrgId LowCardinality(String), + Hour DateTime, + ServiceName LowCardinality(String), + DbSystem LowCardinality(String), + DeploymentEnv LowCardinality(String), + QueryKey String, + QueryLabel SimpleAggregateFunction(any, String), + SampleStatement SimpleAggregateFunction(any, String), + CallCount SimpleAggregateFunction(sum, UInt64), + ErrorCount SimpleAggregateFunction(sum, UInt64), + EstimatedCount SimpleAggregateFunction(sum, Float64), + EstimatedErrorCount SimpleAggregateFunction(sum, Float64), + WeightedDurationSumMs SimpleAggregateFunction(sum, Float64), + DurationQuantiles AggregateFunction(quantilesTDigestWeighted(0.5, 0.95), UInt64, UInt32), + DbNamespace LowCardinality(String) +) +ENGINE = AggregatingMergeTree +PARTITION BY toDate(Hour) +ORDER BY (OrgId, Hour, DeploymentEnv, ServiceName, DbSystem, DbNamespace, QueryKey) +TTL toDate(Hour) + INTERVAL 365 DAY; + +CREATE TABLE IF NOT EXISTS service_map_edges_hourly ( + OrgId LowCardinality(String), + Hour DateTime, + SourceService LowCardinality(String), + TargetService String, + DeploymentEnv LowCardinality(String), + CallCount SimpleAggregateFunction(sum, UInt64), + ErrorCount SimpleAggregateFunction(sum, UInt64), + DurationSumMs SimpleAggregateFunction(sum, Float64), + MaxDurationMs SimpleAggregateFunction(max, Float64), + SampledSpanCount SimpleAggregateFunction(sum, UInt64), + UnsampledSpanCount SimpleAggregateFunction(sum, UInt64), + SampleRateSum SimpleAggregateFunction(sum, Float64) +) +ENGINE = AggregatingMergeTree +PARTITION BY toDate(Hour) +ORDER BY (OrgId, Hour, DeploymentEnv, SourceService, TargetService) +TTL toDate(Hour) + INTERVAL 365 DAY; + +CREATE TABLE IF NOT EXISTS service_map_edges_hourly_ingest ( + OrgId LowCardinality(String), + Hour DateTime, + SourceService LowCardinality(String), + TargetService String, + DeploymentEnv LowCardinality(String), + CallCount UInt64, + ErrorCount UInt64, + DurationSumMs Float64, + MaxDurationMs Float64, + SampledSpanCount UInt64, + UnsampledSpanCount UInt64, + SampleRateSum Float64 +) +ENGINE = Null; + +CREATE TABLE IF NOT EXISTS service_map_spans ( + OrgId LowCardinality(String), + Timestamp DateTime, + TraceId String, + SpanId String, + ParentSpanId String, + ServiceName LowCardinality(String), + SpanKind LowCardinality(String), + Duration UInt64, + StatusCode LowCardinality(String), + TraceState String, + DeploymentEnv LowCardinality(String) +) +ENGINE = MergeTree +PARTITION BY toDate(Timestamp) +ORDER BY (OrgId, TraceId, SpanId, Timestamp) +TTL Timestamp + INTERVAL 30 DAY; + +CREATE TABLE IF NOT EXISTS service_operations_hourly ( + OrgId LowCardinality(String), + Hour DateTime, + ServiceName LowCardinality(String), + DeploymentEnv LowCardinality(String), + SpanName String, + SpanCount SimpleAggregateFunction(sum, UInt64), + EstimatedSpanCount SimpleAggregateFunction(sum, Float64), + ErrorCount SimpleAggregateFunction(sum, UInt64), + EstimatedErrorCount SimpleAggregateFunction(sum, Float64), + DurationSum SimpleAggregateFunction(sum, Float64), + DurationQuantiles AggregateFunction(quantilesTDigest(0.5, 0.95), UInt64), + ClassifiedSpanCount SimpleAggregateFunction(sum, UInt64), + ServerSpanCount SimpleAggregateFunction(sum, UInt64), + RoutedSpanCount SimpleAggregateFunction(sum, UInt64) +) +ENGINE = AggregatingMergeTree +PARTITION BY toYYYYMM(Hour) +ORDER BY (OrgId, ServiceName, DeploymentEnv, Hour, SpanName) +TTL toDate(Hour) + INTERVAL 365 DAY; + +CREATE TABLE IF NOT EXISTS service_operations_minutely ( + OrgId LowCardinality(String), + Minute DateTime, + ServiceName LowCardinality(String), + DeploymentEnv LowCardinality(String), + SpanName String, + SpanCount SimpleAggregateFunction(sum, UInt64), + EstimatedSpanCount SimpleAggregateFunction(sum, Float64), + ErrorCount SimpleAggregateFunction(sum, UInt64), + EstimatedErrorCount SimpleAggregateFunction(sum, Float64), + DurationSum SimpleAggregateFunction(sum, Float64), + DurationQuantiles AggregateFunction(quantilesTDigest(0.5, 0.95), UInt64), + ClassifiedSpanCount SimpleAggregateFunction(sum, UInt64), + ServerSpanCount SimpleAggregateFunction(sum, UInt64), + RoutedSpanCount SimpleAggregateFunction(sum, UInt64) +) +ENGINE = AggregatingMergeTree +PARTITION BY toDate(Minute) +ORDER BY (OrgId, ServiceName, DeploymentEnv, Minute, SpanName) +TTL toDate(Minute) + INTERVAL 90 DAY; + +CREATE TABLE IF NOT EXISTS service_overview_hourly ( + OrgId LowCardinality(String), + Hour DateTime, + ServiceName LowCardinality(String), + DeploymentEnv LowCardinality(String), + ServiceNamespace LowCardinality(String), + CommitSha LowCardinality(String), + SpanCount SimpleAggregateFunction(sum, UInt64), + EstimatedSpanCount SimpleAggregateFunction(sum, Float64), + ErrorCount SimpleAggregateFunction(sum, UInt64), + EstimatedErrorCount SimpleAggregateFunction(sum, Float64), + DurationSum SimpleAggregateFunction(sum, Float64), + DurationQuantiles AggregateFunction(quantilesTDigest(0.5, 0.95, 0.99), UInt64), + FirstSeen SimpleAggregateFunction(min, DateTime), + ApdexSatisfiedCount SimpleAggregateFunction(sum, UInt64), + ApdexToleratingCount SimpleAggregateFunction(sum, UInt64) +) +ENGINE = AggregatingMergeTree +PARTITION BY toYYYYMM(Hour) +ORDER BY (OrgId, ServiceName, Hour, DeploymentEnv, ServiceNamespace, CommitSha) +TTL toDate(Hour) + INTERVAL 365 DAY; + +CREATE TABLE IF NOT EXISTS service_overview_minutely ( + OrgId LowCardinality(String), + Minute DateTime, + ServiceName LowCardinality(String), + DeploymentEnv LowCardinality(String), + ServiceNamespace LowCardinality(String), + CommitSha LowCardinality(String), + SpanCount SimpleAggregateFunction(sum, UInt64), + EstimatedSpanCount SimpleAggregateFunction(sum, Float64), + ErrorCount SimpleAggregateFunction(sum, UInt64), + EstimatedErrorCount SimpleAggregateFunction(sum, Float64), + DurationSum SimpleAggregateFunction(sum, Float64), + DurationQuantiles AggregateFunction(quantilesTDigest(0.5, 0.95, 0.99), UInt64), + FirstSeen SimpleAggregateFunction(min, DateTime), + ApdexSatisfiedCount SimpleAggregateFunction(sum, UInt64), + ApdexToleratingCount SimpleAggregateFunction(sum, UInt64) +) +ENGINE = AggregatingMergeTree +PARTITION BY toDate(Minute) +ORDER BY (OrgId, ServiceName, Minute, DeploymentEnv, ServiceNamespace, CommitSha) +TTL toDate(Minute) + INTERVAL 90 DAY; + +CREATE TABLE IF NOT EXISTS service_overview_spans ( + OrgId LowCardinality(String), + Timestamp DateTime, + ServiceName LowCardinality(String), + Duration UInt64, + StatusCode LowCardinality(String), + TraceState String, + DeploymentEnv LowCardinality(String), + CommitSha LowCardinality(String), + SampleRate Float64 DEFAULT 1, + ServiceNamespace LowCardinality(String), + INDEX idx_service_namespace ServiceNamespace TYPE set(1000) GRANULARITY 4 +) +ENGINE = MergeTree +PARTITION BY toDate(Timestamp) +ORDER BY (OrgId, ServiceName, Timestamp) +TTL Timestamp + INTERVAL 30 DAY; + +CREATE TABLE IF NOT EXISTS service_platforms_hourly ( + OrgId LowCardinality(String), + Hour DateTime, + ServiceName LowCardinality(String), + DeploymentEnv LowCardinality(String), + K8sCluster SimpleAggregateFunction(max, String), + K8sPodName SimpleAggregateFunction(max, String), + K8sDeploymentName SimpleAggregateFunction(max, String), + K8sStatefulSetName SimpleAggregateFunction(max, String), + K8sDaemonSetName SimpleAggregateFunction(max, String), + K8sNamespaceName SimpleAggregateFunction(max, String), + CloudPlatform SimpleAggregateFunction(max, String), + CloudProvider SimpleAggregateFunction(max, String), + FaasName SimpleAggregateFunction(max, String), + MapleSdkType SimpleAggregateFunction(max, String), + ProcessRuntimeName SimpleAggregateFunction(max, String), + SpanCount SimpleAggregateFunction(sum, UInt64) +) +ENGINE = AggregatingMergeTree +PARTITION BY toDate(Hour) +ORDER BY (OrgId, Hour, ServiceName, DeploymentEnv) +TTL toDate(Hour) + INTERVAL 365 DAY; + +CREATE TABLE IF NOT EXISTS service_usage ( + OrgId LowCardinality(String), + ServiceName LowCardinality(String), + Hour DateTime, + LogCount UInt64, + LogSizeBytes UInt64, + TraceCount UInt64, + TraceSizeBytes UInt64, + SumMetricCount UInt64, + SumMetricSizeBytes UInt64, + GaugeMetricCount UInt64, + GaugeMetricSizeBytes UInt64, + HistogramMetricCount UInt64, + HistogramMetricSizeBytes UInt64, + ExpHistogramMetricCount UInt64, + ExpHistogramMetricSizeBytes UInt64 +) +ENGINE = SummingMergeTree +ORDER BY (OrgId, ServiceName, Hour) +TTL Hour + INTERVAL 365 DAY; + +CREATE TABLE IF NOT EXISTS session_events ( + OrgId LowCardinality(String), + SessionId String, + Timestamp DateTime64(9), + Seq UInt32 DEFAULT 0, + Type LowCardinality(String), + Url String DEFAULT '', + TraceId String DEFAULT '', + Level LowCardinality(String) DEFAULT '', + Message String DEFAULT '', + TargetSelector String DEFAULT '', + TargetText String DEFAULT '', + NetMethod LowCardinality(String) DEFAULT '', + NetUrl String DEFAULT '', + NetStatus UInt16 DEFAULT 0, + NetDurationMs UInt32 DEFAULT 0, + ErrorStack String DEFAULT '', + Attributes Map(String, String), + VisitorId String DEFAULT '', + UserId String DEFAULT '', + GroupId String DEFAULT '', + INDEX idx_type Type TYPE set(16) GRANULARITY 4 +) +ENGINE = MergeTree +PARTITION BY toDate(Timestamp) +ORDER BY (OrgId, SessionId, Timestamp, Seq) +TTL toDate(Timestamp) + INTERVAL 30 DAY; + +CREATE TABLE IF NOT EXISTS session_replay_events ( + OrgId LowCardinality(String), + SessionId String, + ChunkSeq UInt32, + Timestamp DateTime64(9), + DurationMs UInt32 DEFAULT 0, + EventCount UInt32 DEFAULT 0, + ByteSize UInt32 DEFAULT 0, + Events String, + IsCheckpoint UInt8 DEFAULT 0 +) +ENGINE = MergeTree +PARTITION BY toDate(Timestamp) +ORDER BY (OrgId, SessionId, ChunkSeq) +TTL toDate(Timestamp) + INTERVAL 30 DAY; + +CREATE TABLE IF NOT EXISTS session_replays ( + OrgId LowCardinality(String), + SessionId String, + StartTime DateTime64(9), + EndTime Nullable(DateTime64(9)), + DurationMs Nullable(UInt32), + Status LowCardinality(String), + UserId String, + UrlInitial String, + UserAgent String, + BrowserName LowCardinality(String), + OsName LowCardinality(String), + DeviceType LowCardinality(String), + Country LowCardinality(String) DEFAULT '', + ServiceName LowCardinality(String), + PageViews UInt32 DEFAULT 0, + ClickCount UInt32 DEFAULT 0, + ErrorCount UInt32 DEFAULT 0, + TraceIds Array(String) DEFAULT [], + ResourceAttributes Map(LowCardinality(String), String), + Version UInt32, + VisitorId String DEFAULT '', + VisitorIsNew UInt8 DEFAULT 0, + UserEmail String DEFAULT '', + UserName String DEFAULT '', + GroupId String DEFAULT '', + GroupName String DEFAULT '', + UserTraits Map(String, String) DEFAULT map(), + Referrer String DEFAULT '', + ReferrerHost LowCardinality(String) DEFAULT '', + UtmSource LowCardinality(String) DEFAULT '', + UtmMedium LowCardinality(String) DEFAULT '', + UtmCampaign LowCardinality(String) DEFAULT '', + UtmTerm String DEFAULT '', + UtmContent String DEFAULT '', + Host LowCardinality(String) DEFAULT '', + EntryPath String DEFAULT '', + ExitPath String DEFAULT '', + Language LowCardinality(String) DEFAULT '', + LastActivityAt Nullable(DateTime64(9)) +) +ENGINE = ReplacingMergeTree +PARTITION BY toDate(StartTime) +ORDER BY (OrgId, SessionId) +TTL toDate(StartTime) + INTERVAL 30 DAY; + +CREATE TABLE IF NOT EXISTS span_metrics_calls_hourly ( + OrgId LowCardinality(String), + Hour DateTime, + ServiceName LowCardinality(String), + MetricName LowCardinality(String), + SpanKind LowCardinality(String), + AttrFingerprint UInt64, + ResourceFingerprint UInt64, + StartTimeUnix DateTime64(9), + LastValue AggregateFunction(argMax, Float64, DateTime64(9)) +) +ENGINE = AggregatingMergeTree +PARTITION BY toDate(Hour) +ORDER BY (OrgId, Hour, ServiceName, MetricName, SpanKind, AttrFingerprint, ResourceFingerprint, StartTimeUnix) +TTL toDate(Hour) + INTERVAL 90 DAY; + +CREATE TABLE IF NOT EXISTS trace_detail_spans ( + OrgId LowCardinality(String), + Timestamp DateTime64(9), + TraceId String, + SpanId String, + ParentSpanId String, + SpanName LowCardinality(String), + SpanKind LowCardinality(String), + ServiceName LowCardinality(String), + Duration UInt64 DEFAULT 0, + StatusCode LowCardinality(String), + StatusMessage String, + SpanAttributes Map(LowCardinality(String), String), + ResourceAttributes Map(LowCardinality(String), String) +) +ENGINE = MergeTree +PARTITION BY toDate(Timestamp) +ORDER BY (OrgId, TraceId, SpanId) +TTL toDate(Timestamp) + INTERVAL 30 DAY; + +CREATE TABLE IF NOT EXISTS trace_facets_hourly ( + OrgId LowCardinality(String), + Hour DateTime, + ServiceName LowCardinality(String), + SpanName String, + HttpMethod LowCardinality(String), + HttpStatusCode LowCardinality(String), + DeploymentEnv LowCardinality(String), + ServiceNamespace LowCardinality(String), + HasError UInt8, + TraceCount SimpleAggregateFunction(sum, UInt64), + DurationMin SimpleAggregateFunction(min, UInt64), + DurationMax SimpleAggregateFunction(max, UInt64), + DurationQuantiles AggregateFunction(quantilesTDigest(0.5, 0.95), UInt64) +) +ENGINE = AggregatingMergeTree +PARTITION BY toYYYYMM(Hour) +ORDER BY (OrgId, Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError) +TTL toDate(Hour) + INTERVAL 30 DAY; + +CREATE TABLE IF NOT EXISTS trace_list_mv ( + OrgId LowCardinality(String), + TraceId String, + Timestamp DateTime, + ServiceName LowCardinality(String), + SpanName String, + SpanKind LowCardinality(String), + Duration UInt64, + StatusCode LowCardinality(String), + HttpMethod LowCardinality(String), + HttpRoute String, + HttpStatusCode LowCardinality(String), + DeploymentEnv LowCardinality(String), + HasError UInt8, + TraceState String, + ServiceNamespace LowCardinality(String), + INDEX idx_service_namespace ServiceNamespace TYPE set(1000) GRANULARITY 4 +) +ENGINE = MergeTree +PARTITION BY toDate(Timestamp) +ORDER BY (OrgId, Timestamp, TraceId) +TTL Timestamp + INTERVAL 30 DAY; + +CREATE TABLE IF NOT EXISTS traces ( + OrgId LowCardinality(String), + Timestamp DateTime64(9), + TraceId String, + SpanId String, + ParentSpanId String, + TraceState String, + SpanName LowCardinality(String), + SpanKind LowCardinality(String), + ServiceName LowCardinality(String), + ResourceSchemaUrl String, + ResourceAttributes Map(LowCardinality(String), String), + ScopeSchemaUrl String, + ScopeName String, + ScopeVersion String, + ScopeAttributes Map(LowCardinality(String), String), + Duration UInt64 DEFAULT 0, + StatusCode LowCardinality(String), + StatusMessage String, + SpanAttributes Map(LowCardinality(String), String), + EventsTimestamp Array(DateTime64(9)), + EventsName Array(LowCardinality(String)), + EventsAttributes Array(Map(LowCardinality(String), String)), + LinksTraceId Array(String), + LinksSpanId Array(String), + LinksTraceState Array(String), + LinksAttributes Array(Map(LowCardinality(String), String)), + SampleRate Float64 DEFAULT multiIf(SpanAttributes['SampleRate'] != '' AND toFloat64OrZero(SpanAttributes['SampleRate']) >= 1.0, toFloat64OrZero(SpanAttributes['SampleRate']), match(TraceState, 'th:[0-9a-f]+'), 1.0 / greatest(1.0 - reinterpretAsUInt64(reverse(unhex(rightPad(extract(TraceState, 'th:([0-9a-f]+)'), 16, '0')))) / pow(2.0, 64), 0.0001), 1.0), + IsEntryPoint UInt8 DEFAULT if(SpanKind IN ('Server', 'Consumer') OR ParentSpanId = '', 1, 0), + ResourceAttributeItems Array(String) DEFAULT arrayMap((k, v) -> concat(k, char(31), v), mapKeys(ResourceAttributes), mapValues(ResourceAttributes)), + ScopeAttributeItems Array(String) DEFAULT arrayMap((k, v) -> concat(k, char(31), v), mapKeys(ScopeAttributes), mapValues(ScopeAttributes)), + SpanAttributeItems Array(String) DEFAULT arrayMap((k, v) -> concat(k, char(31), v), mapKeys(SpanAttributes), mapValues(SpanAttributes)), + INDEX idx_trace_id TraceId TYPE bloom_filter(0.01) GRANULARITY 1, + INDEX idx_span_attr_keys mapKeys(SpanAttributes) TYPE bloom_filter(0.01) GRANULARITY 1, + INDEX idx_span_attr_vals mapValues(SpanAttributes) TYPE bloom_filter(0.01) GRANULARITY 1, + INDEX idx_resource_attr_keys mapKeys(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1, + INDEX idx_resource_attr_vals mapValues(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1, + INDEX idx_scope_attr_keys mapKeys(ScopeAttributes) TYPE bloom_filter(0.01) GRANULARITY 1, + INDEX idx_scope_attr_vals mapValues(ScopeAttributes) TYPE bloom_filter(0.01) GRANULARITY 1 +) +ENGINE = MergeTree +PARTITION BY toDate(Timestamp) +ORDER BY (OrgId, ServiceName, SpanName, toDateTime(Timestamp)) +TTL toDate(Timestamp) + INTERVAL 30 DAY; + +CREATE TABLE IF NOT EXISTS traces_aggregates_hourly ( + OrgId LowCardinality(String), + Hour DateTime, + ServiceName LowCardinality(String), + SpanName LowCardinality(String), + SpanKind LowCardinality(String), + StatusCode LowCardinality(String), + IsEntryPoint UInt8, + DeploymentEnv LowCardinality(String), + WeightedCount SimpleAggregateFunction(sum, Float64), + WeightedDurationSum SimpleAggregateFunction(sum, Float64), + WeightedErrorCount SimpleAggregateFunction(sum, Float64), + DurationQuantiles AggregateFunction(quantilesTDigestWeighted(0.5, 0.95, 0.99), UInt64, UInt32), + DurationMin SimpleAggregateFunction(min, UInt64), + DurationMax SimpleAggregateFunction(max, UInt64) +) +ENGINE = AggregatingMergeTree +PARTITION BY toDate(Hour) +ORDER BY (OrgId, Hour, ServiceName, SpanName, SpanKind, StatusCode, IsEntryPoint, DeploymentEnv) +TTL toDate(Hour) + INTERVAL 365 DAY; + +CREATE MATERIALIZED VIEW IF NOT EXISTS ai_crawler_requests_mv TO ai_crawler_requests AS +SELECT + OrgId, + Timestamp, + TraceId, + ServiceName, + arrayElement(['GPTBot', 'OAI-SearchBot', 'ChatGPT-User', 'ClaudeBot', 'anthropic-ai', 'Claude-SearchBot', 'Claude-User', 'PerplexityBot', 'Perplexity-User', 'Meta-ExternalAgent', 'Meta-WebIndexer', 'Meta-ExternalFetcher', 'Bytespider', 'MistralAI-User', 'CCBot', 'Amazonbot', 'DuckAssistBot', 'cohere-training-data-crawler', 'cohere-ai'], multiSearchFirstIndexCaseInsensitive(coalesce(nullIf(SpanAttributes['user_agent.original'], ''), nullIf(SpanAttributes['http.user_agent'], ''), ''), ['GPTBot', 'OAI-SearchBot', 'ChatGPT-User', 'ClaudeBot', 'anthropic-ai', 'Claude-SearchBot', 'Claude-User', 'PerplexityBot', 'Perplexity-User', 'meta-externalagent', 'meta-webindexer', 'meta-externalfetcher', 'Bytespider', 'MistralAI-User', 'CCBot', 'Amazonbot', 'DuckAssistBot', 'cohere-training-data-crawler', 'cohere-ai'])) AS Crawler, + lower(replaceRegexpOne(coalesce(nullIf(SpanAttributes['server.address'], ''), nullIf(SpanAttributes['http.host'], ''), nullIf(SpanAttributes['net.host.name'], ''), nullIf(domain(SpanAttributes['url.full']), ''), nullIf(domain(SpanAttributes['http.url']), ''), ''), ':[0-9]+$', '')) AS Host, + leftUTF8(coalesce(nullIf(SpanAttributes['url.path'], ''), nullIf(replaceRegexpOne(SpanAttributes['http.target'], '[?#].*$', ''), ''), nullIf(path(SpanAttributes['url.full']), ''), nullIf(path(SpanAttributes['http.url']), ''), ''), 512) AS Path, + toUInt16OrZero(coalesce(nullIf(SpanAttributes['http.response.status_code'], ''), nullIf(SpanAttributes['http.status_code'], ''), '')) AS HttpStatus + FROM traces + WHERE SpanKind = 'Server' + AND multiSearchFirstIndexCaseInsensitive(coalesce(nullIf(SpanAttributes['user_agent.original'], ''), nullIf(SpanAttributes['http.user_agent'], ''), ''), ['GPTBot', 'OAI-SearchBot', 'ChatGPT-User', 'ClaudeBot', 'anthropic-ai', 'Claude-SearchBot', 'Claude-User', 'PerplexityBot', 'Perplexity-User', 'meta-externalagent', 'meta-webindexer', 'meta-externalfetcher', 'Bytespider', 'MistralAI-User', 'CCBot', 'Amazonbot', 'DuckAssistBot', 'cohere-training-data-crawler', 'cohere-ai']) > 0 + AND leftUTF8(coalesce(nullIf(SpanAttributes['url.path'], ''), nullIf(replaceRegexpOne(SpanAttributes['http.target'], '[?#].*$', ''), ''), nullIf(path(SpanAttributes['url.full']), ''), nullIf(path(SpanAttributes['http.url']), ''), ''), 512) != ''; + +CREATE MATERIALIZED VIEW IF NOT EXISTS ai_trace_index_mv TO ai_trace_index AS +SELECT + OrgId, + Timestamp, + TraceId, + SpanAttributes['maple_ai.session.id'] AS SessionId, + SpanAttributes['maple_ai.vendor.id'] AS VendorId, + ServiceName, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv, + coalesce(nullIf(SpanAttributes['gen_ai.response.model'], ''), nullIf(SpanAttributes['gen_ai.request.model'], ''), nullIf(SpanAttributes['ai.response.model'], ''), nullIf(SpanAttributes['ai.model.id'], ''), SpanAttributes['llm.model_name']) AS Model, + coalesce(nullIf(SpanAttributes['gen_ai.agent.name'], ''), SpanAttributes['ai.telemetry.functionId']) AS AgentName, + coalesce(nullIf(SpanAttributes['gen_ai.tool.name'], ''), nullIf(SpanAttributes['ai.toolCall.name'], ''), SpanAttributes['tool.name']) AS ToolName, + SpanId, + ParentSpanId, + Duration, + toUInt8(((StatusCode = 'Error' OR SpanAttributes['error.type'] != '') OR SpanAttributes['gen_ai.response.status'] IN ('failed', 'error'))) AS IsError, + toUInt8((coalesce(nullIf(SpanAttributes['gen_ai.operation.name'], ''), multiIf(SpanAttributes['openinference.span.kind'] = 'LLM', 'chat', SpanAttributes['openinference.span.kind'] = 'TOOL', 'execute_tool', SpanAttributes['openinference.span.kind'] = 'AGENT', 'invoke_agent', SpanAttributes['openinference.span.kind'] = 'EMBEDDING', 'embeddings', SpanAttributes['openinference.span.kind'] = 'RETRIEVER', 'retrieval', '')) IN ('chat', 'generate_content', 'text_completion', 'fetch_response') OR (((coalesce(nullIf(SpanAttributes['gen_ai.operation.name'], ''), multiIf(SpanAttributes['openinference.span.kind'] = 'LLM', 'chat', SpanAttributes['openinference.span.kind'] = 'TOOL', 'execute_tool', SpanAttributes['openinference.span.kind'] = 'AGENT', 'invoke_agent', SpanAttributes['openinference.span.kind'] = 'EMBEDDING', 'embeddings', SpanAttributes['openinference.span.kind'] = 'RETRIEVER', 'retrieval', '')) NOT IN ('chat', 'generate_content', 'text_completion', 'fetch_response', 'embeddings', 'retrieval', 'execute_tool', 'invoke_agent', 'create_agent', 'invoke_workflow', 'plan', 'agent_step') AND NOT ((coalesce(nullIf(SpanAttributes['gen_ai.tool.name'], ''), nullIf(SpanAttributes['ai.toolCall.name'], ''), SpanAttributes['tool.name']) != '' OR lower(SpanName) LIKE '%tool%'))) AND NOT ((lower(SpanName) LIKE '%agent%' OR lower(SpanName) LIKE '%workflow%'))) AND (coalesce(nullIf(SpanAttributes['gen_ai.response.model'], ''), nullIf(SpanAttributes['gen_ai.request.model'], ''), nullIf(SpanAttributes['ai.response.model'], ''), nullIf(SpanAttributes['ai.model.id'], ''), SpanAttributes['llm.model_name']) != '' OR (lower(SpanName) LIKE '%chat%' OR lower(SpanName) LIKE '%completion%'))))) AS IsLlmCall, + toUInt8((coalesce(nullIf(SpanAttributes['gen_ai.operation.name'], ''), multiIf(SpanAttributes['openinference.span.kind'] = 'LLM', 'chat', SpanAttributes['openinference.span.kind'] = 'TOOL', 'execute_tool', SpanAttributes['openinference.span.kind'] = 'AGENT', 'invoke_agent', SpanAttributes['openinference.span.kind'] = 'EMBEDDING', 'embeddings', SpanAttributes['openinference.span.kind'] = 'RETRIEVER', 'retrieval', '')) IN ('execute_tool') OR (coalesce(nullIf(SpanAttributes['gen_ai.operation.name'], ''), multiIf(SpanAttributes['openinference.span.kind'] = 'LLM', 'chat', SpanAttributes['openinference.span.kind'] = 'TOOL', 'execute_tool', SpanAttributes['openinference.span.kind'] = 'AGENT', 'invoke_agent', SpanAttributes['openinference.span.kind'] = 'EMBEDDING', 'embeddings', SpanAttributes['openinference.span.kind'] = 'RETRIEVER', 'retrieval', '')) NOT IN ('chat', 'generate_content', 'text_completion', 'fetch_response', 'embeddings', 'retrieval', 'execute_tool', 'invoke_agent', 'create_agent', 'invoke_workflow', 'plan', 'agent_step') AND (coalesce(nullIf(SpanAttributes['gen_ai.tool.name'], ''), nullIf(SpanAttributes['ai.toolCall.name'], ''), SpanAttributes['tool.name']) != '' OR lower(SpanName) LIKE '%tool%')))) AS IsToolCall, + multiIf(SpanAttributes['maple_ai.vendor.id'] IN ('vercel_ai_sdk', 'maple'), greatest(toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.input_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.prompt_tokens'], ''), nullIf(SpanAttributes['ai.usage.inputTokens'], ''), nullIf(SpanAttributes['ai.usage.promptTokens'], ''), SpanAttributes['llm.token_count.prompt'])), toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.cache_read.input_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.input_tokens.cached'], ''), nullIf(SpanAttributes['ai.usage.cachedInputTokens'], ''), nullIf(SpanAttributes['ai.usage.inputTokenDetails.cacheReadTokens'], ''), SpanAttributes['llm.token_count.prompt_details.cache_read'])) + toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.cache_creation.input_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.cache_write.input_tokens'], ''), SpanAttributes['ai.usage.inputTokenDetails.cacheWriteTokens']))), coalesce(nullIf(SpanAttributes['gen_ai.provider.name'], ''), nullIf(SpanAttributes['gen_ai.system'], ''), nullIf(SpanAttributes['ai.model.provider'], ''), nullIf(SpanAttributes['llm.provider'], ''), SpanAttributes['llm.system']) IN ('openai', 'gcp.gemini', 'gemini', 'gcp.vertex_ai', 'vertex_ai', 'openrouter'), greatest(toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.input_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.prompt_tokens'], ''), nullIf(SpanAttributes['ai.usage.inputTokens'], ''), nullIf(SpanAttributes['ai.usage.promptTokens'], ''), SpanAttributes['llm.token_count.prompt'])), toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.cache_read.input_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.input_tokens.cached'], ''), nullIf(SpanAttributes['ai.usage.cachedInputTokens'], ''), nullIf(SpanAttributes['ai.usage.inputTokenDetails.cacheReadTokens'], ''), SpanAttributes['llm.token_count.prompt_details.cache_read'])) + toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.cache_creation.input_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.cache_write.input_tokens'], ''), SpanAttributes['ai.usage.inputTokenDetails.cacheWriteTokens']))), coalesce(nullIf(SpanAttributes['gen_ai.provider.name'], ''), nullIf(SpanAttributes['gen_ai.system'], ''), nullIf(SpanAttributes['ai.model.provider'], ''), nullIf(SpanAttributes['llm.provider'], ''), SpanAttributes['llm.system']) IN ('anthropic'), toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.input_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.prompt_tokens'], ''), nullIf(SpanAttributes['ai.usage.inputTokens'], ''), nullIf(SpanAttributes['ai.usage.promptTokens'], ''), SpanAttributes['llm.token_count.prompt'])) + toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.cache_read.input_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.input_tokens.cached'], ''), nullIf(SpanAttributes['ai.usage.cachedInputTokens'], ''), nullIf(SpanAttributes['ai.usage.inputTokenDetails.cacheReadTokens'], ''), SpanAttributes['llm.token_count.prompt_details.cache_read'])) + toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.cache_creation.input_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.cache_write.input_tokens'], ''), SpanAttributes['ai.usage.inputTokenDetails.cacheWriteTokens'])), greatest(toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.input_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.prompt_tokens'], ''), nullIf(SpanAttributes['ai.usage.inputTokens'], ''), nullIf(SpanAttributes['ai.usage.promptTokens'], ''), SpanAttributes['llm.token_count.prompt'])), toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.cache_read.input_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.input_tokens.cached'], ''), nullIf(SpanAttributes['ai.usage.cachedInputTokens'], ''), nullIf(SpanAttributes['ai.usage.inputTokenDetails.cacheReadTokens'], ''), SpanAttributes['llm.token_count.prompt_details.cache_read'])) + toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.cache_creation.input_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.cache_write.input_tokens'], ''), SpanAttributes['ai.usage.inputTokenDetails.cacheWriteTokens'])))) + multiIf(SpanAttributes['maple_ai.vendor.id'] IN ('vercel_ai_sdk', 'maple'), greatest(toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.output_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.completion_tokens'], ''), nullIf(SpanAttributes['ai.usage.outputTokens'], ''), nullIf(SpanAttributes['ai.usage.completionTokens'], ''), SpanAttributes['llm.token_count.completion'])), toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.reasoning.output_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.output_tokens.reasoning'], ''), nullIf(SpanAttributes['ai.usage.reasoningTokens'], ''), nullIf(SpanAttributes['ai.usage.outputTokenDetails.reasoningTokens'], ''), SpanAttributes['llm.token_count.completion_details.reasoning']))), coalesce(nullIf(SpanAttributes['gen_ai.provider.name'], ''), nullIf(SpanAttributes['gen_ai.system'], ''), nullIf(SpanAttributes['ai.model.provider'], ''), nullIf(SpanAttributes['llm.provider'], ''), SpanAttributes['llm.system']) IN ('anthropic', 'openai', 'openrouter'), greatest(toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.output_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.completion_tokens'], ''), nullIf(SpanAttributes['ai.usage.outputTokens'], ''), nullIf(SpanAttributes['ai.usage.completionTokens'], ''), SpanAttributes['llm.token_count.completion'])), toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.reasoning.output_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.output_tokens.reasoning'], ''), nullIf(SpanAttributes['ai.usage.reasoningTokens'], ''), nullIf(SpanAttributes['ai.usage.outputTokenDetails.reasoningTokens'], ''), SpanAttributes['llm.token_count.completion_details.reasoning']))), coalesce(nullIf(SpanAttributes['gen_ai.provider.name'], ''), nullIf(SpanAttributes['gen_ai.system'], ''), nullIf(SpanAttributes['ai.model.provider'], ''), nullIf(SpanAttributes['llm.provider'], ''), SpanAttributes['llm.system']) IN ('gcp.gemini', 'gemini', 'gcp.vertex_ai', 'vertex_ai'), toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.output_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.completion_tokens'], ''), nullIf(SpanAttributes['ai.usage.outputTokens'], ''), nullIf(SpanAttributes['ai.usage.completionTokens'], ''), SpanAttributes['llm.token_count.completion'])) + toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.reasoning.output_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.output_tokens.reasoning'], ''), nullIf(SpanAttributes['ai.usage.reasoningTokens'], ''), nullIf(SpanAttributes['ai.usage.outputTokenDetails.reasoningTokens'], ''), SpanAttributes['llm.token_count.completion_details.reasoning'])), greatest(toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.output_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.completion_tokens'], ''), nullIf(SpanAttributes['ai.usage.outputTokens'], ''), nullIf(SpanAttributes['ai.usage.completionTokens'], ''), SpanAttributes['llm.token_count.completion'])), toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.reasoning.output_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.output_tokens.reasoning'], ''), nullIf(SpanAttributes['ai.usage.reasoningTokens'], ''), nullIf(SpanAttributes['ai.usage.outputTokenDetails.reasoningTokens'], ''), SpanAttributes['llm.token_count.completion_details.reasoning'])))) AS Tokens, + toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.cost'], ''), nullIf(SpanAttributes['gen_ai.usage.total_cost'], ''), SpanAttributes['llm.cost.total'])) AS Cost, + coalesce(nullIf(SpanAttributes['gen_ai.response.id'], ''), SpanAttributes['ai.response.id']) AS ResponseId, + SpanAttributes['maple_ai.vendor.version'] AS VendorVersion, + multiIf(SpanAttributes['maple_ai.vendor.id'] IN ('vercel_ai_sdk', 'maple'), greatest(0, toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.input_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.prompt_tokens'], ''), nullIf(SpanAttributes['ai.usage.inputTokens'], ''), nullIf(SpanAttributes['ai.usage.promptTokens'], ''), SpanAttributes['llm.token_count.prompt'])) - toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.cache_read.input_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.input_tokens.cached'], ''), nullIf(SpanAttributes['ai.usage.cachedInputTokens'], ''), nullIf(SpanAttributes['ai.usage.inputTokenDetails.cacheReadTokens'], ''), SpanAttributes['llm.token_count.prompt_details.cache_read'])) - toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.cache_creation.input_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.cache_write.input_tokens'], ''), SpanAttributes['ai.usage.inputTokenDetails.cacheWriteTokens']))), coalesce(nullIf(SpanAttributes['gen_ai.provider.name'], ''), nullIf(SpanAttributes['gen_ai.system'], ''), nullIf(SpanAttributes['ai.model.provider'], ''), nullIf(SpanAttributes['llm.provider'], ''), SpanAttributes['llm.system']) IN ('openai', 'gcp.gemini', 'gemini', 'gcp.vertex_ai', 'vertex_ai', 'openrouter'), greatest(0, toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.input_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.prompt_tokens'], ''), nullIf(SpanAttributes['ai.usage.inputTokens'], ''), nullIf(SpanAttributes['ai.usage.promptTokens'], ''), SpanAttributes['llm.token_count.prompt'])) - toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.cache_read.input_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.input_tokens.cached'], ''), nullIf(SpanAttributes['ai.usage.cachedInputTokens'], ''), nullIf(SpanAttributes['ai.usage.inputTokenDetails.cacheReadTokens'], ''), SpanAttributes['llm.token_count.prompt_details.cache_read'])) - toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.cache_creation.input_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.cache_write.input_tokens'], ''), SpanAttributes['ai.usage.inputTokenDetails.cacheWriteTokens']))), coalesce(nullIf(SpanAttributes['gen_ai.provider.name'], ''), nullIf(SpanAttributes['gen_ai.system'], ''), nullIf(SpanAttributes['ai.model.provider'], ''), nullIf(SpanAttributes['llm.provider'], ''), SpanAttributes['llm.system']) IN ('anthropic'), toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.input_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.prompt_tokens'], ''), nullIf(SpanAttributes['ai.usage.inputTokens'], ''), nullIf(SpanAttributes['ai.usage.promptTokens'], ''), SpanAttributes['llm.token_count.prompt'])), greatest(0, toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.input_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.prompt_tokens'], ''), nullIf(SpanAttributes['ai.usage.inputTokens'], ''), nullIf(SpanAttributes['ai.usage.promptTokens'], ''), SpanAttributes['llm.token_count.prompt'])) - toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.cache_read.input_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.input_tokens.cached'], ''), nullIf(SpanAttributes['ai.usage.cachedInputTokens'], ''), nullIf(SpanAttributes['ai.usage.inputTokenDetails.cacheReadTokens'], ''), SpanAttributes['llm.token_count.prompt_details.cache_read'])) - toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.cache_creation.input_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.cache_write.input_tokens'], ''), SpanAttributes['ai.usage.inputTokenDetails.cacheWriteTokens'])))) AS InputTokens, + toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.cache_read.input_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.input_tokens.cached'], ''), nullIf(SpanAttributes['ai.usage.cachedInputTokens'], ''), nullIf(SpanAttributes['ai.usage.inputTokenDetails.cacheReadTokens'], ''), SpanAttributes['llm.token_count.prompt_details.cache_read'])) AS CacheReadTokens, + toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.cache_creation.input_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.cache_write.input_tokens'], ''), SpanAttributes['ai.usage.inputTokenDetails.cacheWriteTokens'])) AS CacheWriteTokens, + multiIf(SpanAttributes['maple_ai.vendor.id'] IN ('vercel_ai_sdk', 'maple'), greatest(0, toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.output_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.completion_tokens'], ''), nullIf(SpanAttributes['ai.usage.outputTokens'], ''), nullIf(SpanAttributes['ai.usage.completionTokens'], ''), SpanAttributes['llm.token_count.completion'])) - toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.reasoning.output_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.output_tokens.reasoning'], ''), nullIf(SpanAttributes['ai.usage.reasoningTokens'], ''), nullIf(SpanAttributes['ai.usage.outputTokenDetails.reasoningTokens'], ''), SpanAttributes['llm.token_count.completion_details.reasoning']))), coalesce(nullIf(SpanAttributes['gen_ai.provider.name'], ''), nullIf(SpanAttributes['gen_ai.system'], ''), nullIf(SpanAttributes['ai.model.provider'], ''), nullIf(SpanAttributes['llm.provider'], ''), SpanAttributes['llm.system']) IN ('anthropic', 'openai', 'openrouter'), greatest(0, toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.output_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.completion_tokens'], ''), nullIf(SpanAttributes['ai.usage.outputTokens'], ''), nullIf(SpanAttributes['ai.usage.completionTokens'], ''), SpanAttributes['llm.token_count.completion'])) - toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.reasoning.output_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.output_tokens.reasoning'], ''), nullIf(SpanAttributes['ai.usage.reasoningTokens'], ''), nullIf(SpanAttributes['ai.usage.outputTokenDetails.reasoningTokens'], ''), SpanAttributes['llm.token_count.completion_details.reasoning']))), coalesce(nullIf(SpanAttributes['gen_ai.provider.name'], ''), nullIf(SpanAttributes['gen_ai.system'], ''), nullIf(SpanAttributes['ai.model.provider'], ''), nullIf(SpanAttributes['llm.provider'], ''), SpanAttributes['llm.system']) IN ('gcp.gemini', 'gemini', 'gcp.vertex_ai', 'vertex_ai'), toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.output_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.completion_tokens'], ''), nullIf(SpanAttributes['ai.usage.outputTokens'], ''), nullIf(SpanAttributes['ai.usage.completionTokens'], ''), SpanAttributes['llm.token_count.completion'])), greatest(0, toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.output_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.completion_tokens'], ''), nullIf(SpanAttributes['ai.usage.outputTokens'], ''), nullIf(SpanAttributes['ai.usage.completionTokens'], ''), SpanAttributes['llm.token_count.completion'])) - toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.reasoning.output_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.output_tokens.reasoning'], ''), nullIf(SpanAttributes['ai.usage.reasoningTokens'], ''), nullIf(SpanAttributes['ai.usage.outputTokenDetails.reasoningTokens'], ''), SpanAttributes['llm.token_count.completion_details.reasoning'])))) AS OutputTokens, + toFloat64OrZero(coalesce(nullIf(SpanAttributes['gen_ai.usage.reasoning.output_tokens'], ''), nullIf(SpanAttributes['gen_ai.usage.output_tokens.reasoning'], ''), nullIf(SpanAttributes['ai.usage.reasoningTokens'], ''), nullIf(SpanAttributes['ai.usage.outputTokenDetails.reasoningTokens'], ''), SpanAttributes['llm.token_count.completion_details.reasoning'])) AS ReasoningTokens, + SpanAttributes['error.type'] AS ErrorType, + leftUTF8(StatusMessage, 400) AS StatusMessage, + leftUTF8(coalesce(nullIf(SpanAttributes['gen_ai.tool.description'], ''), SpanAttributes['tool.description']), 2000) AS ToolDescription, + if((((StatusCode = 'Error' OR SpanAttributes['error.type'] != '') OR SpanAttributes['gen_ai.response.status'] IN ('failed', 'error')) AND (coalesce(nullIf(SpanAttributes['gen_ai.operation.name'], ''), multiIf(SpanAttributes['openinference.span.kind'] = 'LLM', 'chat', SpanAttributes['openinference.span.kind'] = 'TOOL', 'execute_tool', SpanAttributes['openinference.span.kind'] = 'AGENT', 'invoke_agent', SpanAttributes['openinference.span.kind'] = 'EMBEDDING', 'embeddings', SpanAttributes['openinference.span.kind'] = 'RETRIEVER', 'retrieval', '')) IN ('execute_tool') OR (coalesce(nullIf(SpanAttributes['gen_ai.operation.name'], ''), multiIf(SpanAttributes['openinference.span.kind'] = 'LLM', 'chat', SpanAttributes['openinference.span.kind'] = 'TOOL', 'execute_tool', SpanAttributes['openinference.span.kind'] = 'AGENT', 'invoke_agent', SpanAttributes['openinference.span.kind'] = 'EMBEDDING', 'embeddings', SpanAttributes['openinference.span.kind'] = 'RETRIEVER', 'retrieval', '')) NOT IN ('chat', 'generate_content', 'text_completion', 'fetch_response', 'embeddings', 'retrieval', 'execute_tool', 'invoke_agent', 'create_agent', 'invoke_workflow', 'plan', 'agent_step') AND (coalesce(nullIf(SpanAttributes['gen_ai.tool.name'], ''), nullIf(SpanAttributes['ai.toolCall.name'], ''), SpanAttributes['tool.name']) != '' OR lower(SpanName) LIKE '%tool%')))), leftUTF8(coalesce(nullIf(SpanAttributes['gen_ai.tool.call.result'], ''), SpanAttributes['ai.toolCall.result']), 1000), '') AS FailedToolCallResult, + if(((StatusCode = 'Error' OR SpanAttributes['error.type'] != '') OR SpanAttributes['gen_ai.response.status'] IN ('failed', 'error')), cityHash64(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(leftUTF8(coalesce(nullIf(if((((StatusCode = 'Error' OR SpanAttributes['error.type'] != '') OR SpanAttributes['gen_ai.response.status'] IN ('failed', 'error')) AND (coalesce(nullIf(SpanAttributes['gen_ai.operation.name'], ''), multiIf(SpanAttributes['openinference.span.kind'] = 'LLM', 'chat', SpanAttributes['openinference.span.kind'] = 'TOOL', 'execute_tool', SpanAttributes['openinference.span.kind'] = 'AGENT', 'invoke_agent', SpanAttributes['openinference.span.kind'] = 'EMBEDDING', 'embeddings', SpanAttributes['openinference.span.kind'] = 'RETRIEVER', 'retrieval', '')) IN ('execute_tool') OR (coalesce(nullIf(SpanAttributes['gen_ai.operation.name'], ''), multiIf(SpanAttributes['openinference.span.kind'] = 'LLM', 'chat', SpanAttributes['openinference.span.kind'] = 'TOOL', 'execute_tool', SpanAttributes['openinference.span.kind'] = 'AGENT', 'invoke_agent', SpanAttributes['openinference.span.kind'] = 'EMBEDDING', 'embeddings', SpanAttributes['openinference.span.kind'] = 'RETRIEVER', 'retrieval', '')) NOT IN ('chat', 'generate_content', 'text_completion', 'fetch_response', 'embeddings', 'retrieval', 'execute_tool', 'invoke_agent', 'create_agent', 'invoke_workflow', 'plan', 'agent_step') AND (coalesce(nullIf(SpanAttributes['gen_ai.tool.name'], ''), nullIf(SpanAttributes['ai.toolCall.name'], ''), SpanAttributes['tool.name']) != '' OR lower(SpanName) LIKE '%tool%')))), leftUTF8(coalesce(nullIf(SpanAttributes['gen_ai.tool.call.result'], ''), SpanAttributes['ai.toolCall.result']), 1000), ''), ''), leftUTF8(StatusMessage, 400)), 400), '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}', 'EMAIL'), 'https?://[^/ )"]+', ''), '/(Users|home)/[^/ ]+', '/~'), '[?][A-Za-z0-9_]+=[^ )"]*', '?#'), '\'[^\' ]*/[^\' ]*\'|\'[^\' ]{25,}\'', '\'#\''), '"[^" ]*/[^" ]*"|"[^" ]{25,}"', '"#"'), '`[^` ]*/[^` ]*`|`[^` ]{25,}`', '`#`'), '[0-9a-fA-F-]{6,}|[0-9]+', '#')), 0) AS ErrorFingerprint + FROM traces + WHERE SpanAttributes['maple_ai.vendor.id'] != ''; + +CREATE MATERIALIZED VIEW IF NOT EXISTS error_events_by_time_mv TO error_events_by_time AS +WITH + arrayFirstIndex(n -> n = 'exception', EventsName) AS _ei, + -- Only fill the old Unknown Error bucket. Event values (including + -- empty fields) and spans with StatusMessage keep every hash input. + _ei = 0 AND StatusMessage = '' AS _useAttrs, + if( + _ei > 0, EventsAttributes[_ei]['exception.type'], + if(_useAttrs, coalesce(nullIf(SpanAttributes['exception.type'], ''), SpanAttributes['error.type']), '') + ) AS _exType, + if( + _ei > 0, EventsAttributes[_ei]['exception.message'], + if(_useAttrs, coalesce(nullIf(SpanAttributes['exception.message'], ''), SpanAttributes['error.message']), StatusMessage) + ) AS _exMsg, + if( + _ei > 0, EventsAttributes[_ei]['exception.stacktrace'], + if(_useAttrs, SpanAttributes['exception.stacktrace'], '') + ) AS _exStack, + if(_useAttrs, _exMsg, StatusMessage) AS _msgText, + -- Frame lines are matched by SHAPE, not by "contains :NUMBER". The old + -- rule accepted any line with a colon-digit, which let non-frame lines + -- in: Drizzle's `params: ` line, and the `Type: message` + -- header (`Code: 62`, `position 1628`, embedded timestamps). Row values + -- and message text then entered the hash and split one bug into + -- thousands of issues — 23,035 fingerprints for six real + -- AnomalyPersistenceError call sites, 15,051 for thirteen DatabaseError + -- ones. + -- + -- The pattern is rendered from FRAME_LINE_PATTERN in fingerprint.ts, + -- as is every redaction below. They used to be hand-copied here, which + -- let the reference implementation the tests exercise drift away from + -- the SQL that actually runs, silently. + arraySlice( + arrayFilter( + line -> match(line, '^[ \\t]*at |^[ \\t]*File "|^[ \\t]+from [^ ]+:[0-9]+|^[^ \\t@]+@[^ \\t]*:[0-9]+|^[ \\t]+[^ \\t]+\\.(go|rs):[0-9]+|^[0-9]+ +\\S.* +0x[0-9a-fA-F]+'), + splitByChar('\n', _exStack) + ), + 1, 3 + ) AS _rawFrames, + -- Redact every volatile token a frame line can carry: the URL origin + -- (so preview hosts share one fingerprint), Vite's 8-char bundle + -- content hash (so a deploy does not re-split every triaged browser and + -- Worker issue), then line numbers, hex pointers and long id runs. See + -- FRAME_REDACTIONS for the order and the reasoning. + arrayMap( + line -> replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(line, 'https?://[^/ )]+', ''), '-[A-Za-z0-9_-]{8}\\.js', '.js'), '-[A-Za-z0-9_-]{8}\\.css', '.css'), ':[0-9]+|line [0-9]+|0x[0-9a-fA-F]+|[0-9a-fA-F]{8,}|[0-9]{6,}', ''), + _rawFrames + ) AS _topFrames, + if(length(_topFrames) > 0, _topFrames[1], '') AS _topFrame, + arrayStringConcat(_topFrames, '\n') AS _fpFrames, + -- JSON detection for the message signature below. + isValidJSON(_msgText) AS _isJson, + _isJson AND JSONType(_msgText) = 'Object' AS _isJsonObj, + -- General, KEY-NAME-AGNOSTIC canonical signature: iterate ALL top-level + -- keys, redact volatile tokens (long hex / numbers) in each raw value, then + -- sort by "key=value" so key order & whitespace don't matter. No assumption + -- about which keys exist — works for any producer's JSON shape. (Nested + -- objects are hashed as their raw substring; only top-level is canonicalized.) + arrayStringConcat( + arraySort( + arrayMap( + kv -> concat(kv.1, '=', replaceRegexpAll(kv.2, '[0-9a-fA-F]{8,}|[0-9]+', '#')), + JSONExtractKeysAndValuesRaw(_msgText) + ) + ), + '|' + ) AS _jsonSig, + -- The message signature is folded in ALWAYS, not only when there are no + -- frames. Bundled runtimes minify every module into one file, so the top + -- three frames of a Worker error are `toDatabaseError (worker.js)` for + -- every failing query alike: on frames alone, 25 distinct DatabaseError + -- bugs (316k occurrences) collapse into a single issue. The signature + -- restores that discrimination, and it cannot reinflate cardinality the + -- way a raw prefix would because everything variable is redacted first: + -- emails, URL origins, home directories, query strings, quoted values, + -- then ids and every digit run. See MSG_TEXT_REDACTIONS for the order, + -- what is deliberately kept, and the one residual it cannot reach. + multiIf( + _isJsonObj, _jsonSig, + substringUTF8( + replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(substringUTF8(_msgText, 1, 400), '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}', 'EMAIL'), 'https?://[^/ )"]+', ''), '/(Users|home)/[^/ ]+', '/~'), '[?][A-Za-z0-9_]+=[^ )"]*', '?#'), '\'[^\' ]*/[^\' ]*\'|\'[^\' ]{25,}\'', '\'#\''), '"[^" ]*/[^" ]*"|"[^" ]{25,}"', '"#"'), '`[^` ]*/[^` ]*`|`[^` ]{25,}`', '`#`'), '[0-9a-fA-F-]{6,}|[0-9]+', '#'), + 1, 120 + ) + ) AS _msgSig, + -- Display-only, best-effort human label (decoupled from the fingerprint: + -- many labels may map to one hash). The broad key list here is a DISPLAY + -- heuristic only; the fingerprint above makes no key-name assumption. + multiIf( + JSONExtractString(_msgText, 'title') != '', JSONExtractString(_msgText, 'title'), + JSONExtractString(_msgText, 'message') != '', JSONExtractString(_msgText, 'message'), + JSONExtractString(_msgText, 'error') != '', JSONExtractString(_msgText, 'error'), + JSONExtractString(_msgText, '_tag') != '', JSONExtractString(_msgText, '_tag'), + JSONExtractString(_msgText, 'reason') != '', JSONExtractString(_msgText, 'reason'), + JSONExtractString(_msgText, 'name') != '', JSONExtractString(_msgText, 'name'), + JSONExtractString(_msgText, 'type') != '', extract(JSONExtractString(_msgText, 'type'), '([^/]+)$'), + 'JSON error' + ) AS _jsonLabel, + multiIf( + _msgText = '', 'Unknown Error', + position(_msgText, '{ readonly') = 1 OR position(_msgText, '└─') > 0, + if( + extract(_msgText, 'readonly (\\w+)') != '', + concat('Schema parse error: ', extract(_msgText, 'readonly (\\w+)')), + 'Schema parse error' + ), + _isJsonObj OR position(_msgText, '[') = 1, _jsonLabel, + left(_msgText, multiIf( + position(_msgText, ': ') > 3, toInt64(position(_msgText, ': ')) - 1, + position(_msgText, ' (') > 3, toInt64(position(_msgText, ' (')) - 1, + position(_msgText, '\n') > 3, toInt64(position(_msgText, '\n')) - 1, + least(toInt64(length(_msgText)), 150) + )) + ) AS _statusLabel, + if(_exType != '', _exType, _statusLabel) AS _errorLabel, + -- Both semconv spellings; the current key wins when both are present. + toUInt16OrZero( + if( + SpanAttributes['http.response.status_code'] != '', + SpanAttributes['http.response.status_code'], + SpanAttributes['http.status_code'] + ) + ) AS _httpStatus + SELECT + OrgId, + toDateTime(Timestamp) AS Timestamp, + TraceId, + SpanId, + ParentSpanId, + ServiceName, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv, + _exType AS ExceptionType, + _exMsg AS ExceptionMessage, + _exStack AS ExceptionStacktrace, + _topFrame AS TopFrame, + cityHash64(OrgId, ServiceName, _exType, _fpFrames, _msgSig) AS FingerprintHash, + StatusMessage, + Duration, + _errorLabel AS ErrorLabel, + ResourceAttributes['service.version'] AS ServiceVersion + FROM traces + WHERE StatusCode = 'Error' + -- Client-side runtimes (notably the native Cloudflare Workers + -- observability) mark ANY non-2xx fetch span as Error, so 404s from bot + -- traffic arrived here as unlabelled "Unknown Error" issues. Drop a + -- span only when all hold: 4xx, no exception event, no exception.type + -- attribute, and no error.type beyond the status code itself (HTTP + -- semconv sets error.type to the bare status on a non-2xx response, + -- which carries no exception). 5xx and anything carrying a real + -- exception still count, and SpanKind is deliberately not consulted — + -- these are Client spans. + AND NOT ( + _httpStatus >= 400 AND _httpStatus < 500 + AND _ei = 0 + AND SpanAttributes['exception.type'] = '' + AND (SpanAttributes['error.type'] = '' OR SpanAttributes['error.type'] = toString(_httpStatus)) + ); + +CREATE MATERIALIZED VIEW IF NOT EXISTS error_events_mv TO error_events AS +WITH + arrayFirstIndex(n -> n = 'exception', EventsName) AS _ei, + -- Only fill the old Unknown Error bucket. Event values (including + -- empty fields) and spans with StatusMessage keep every hash input. + _ei = 0 AND StatusMessage = '' AS _useAttrs, + if( + _ei > 0, EventsAttributes[_ei]['exception.type'], + if(_useAttrs, coalesce(nullIf(SpanAttributes['exception.type'], ''), SpanAttributes['error.type']), '') + ) AS _exType, + if( + _ei > 0, EventsAttributes[_ei]['exception.message'], + if(_useAttrs, coalesce(nullIf(SpanAttributes['exception.message'], ''), SpanAttributes['error.message']), StatusMessage) + ) AS _exMsg, + if( + _ei > 0, EventsAttributes[_ei]['exception.stacktrace'], + if(_useAttrs, SpanAttributes['exception.stacktrace'], '') + ) AS _exStack, + if(_useAttrs, _exMsg, StatusMessage) AS _msgText, + -- Frame lines are matched by SHAPE, not by "contains :NUMBER". The old + -- rule accepted any line with a colon-digit, which let non-frame lines + -- in: Drizzle's `params: ` line, and the `Type: message` + -- header (`Code: 62`, `position 1628`, embedded timestamps). Row values + -- and message text then entered the hash and split one bug into + -- thousands of issues — 23,035 fingerprints for six real + -- AnomalyPersistenceError call sites, 15,051 for thirteen DatabaseError + -- ones. + -- + -- The pattern is rendered from FRAME_LINE_PATTERN in fingerprint.ts, + -- as is every redaction below. They used to be hand-copied here, which + -- let the reference implementation the tests exercise drift away from + -- the SQL that actually runs, silently. + arraySlice( + arrayFilter( + line -> match(line, '^[ \\t]*at |^[ \\t]*File "|^[ \\t]+from [^ ]+:[0-9]+|^[^ \\t@]+@[^ \\t]*:[0-9]+|^[ \\t]+[^ \\t]+\\.(go|rs):[0-9]+|^[0-9]+ +\\S.* +0x[0-9a-fA-F]+'), + splitByChar('\n', _exStack) + ), + 1, 3 + ) AS _rawFrames, + -- Redact every volatile token a frame line can carry: the URL origin + -- (so preview hosts share one fingerprint), Vite's 8-char bundle + -- content hash (so a deploy does not re-split every triaged browser and + -- Worker issue), then line numbers, hex pointers and long id runs. See + -- FRAME_REDACTIONS for the order and the reasoning. + arrayMap( + line -> replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(line, 'https?://[^/ )]+', ''), '-[A-Za-z0-9_-]{8}\\.js', '.js'), '-[A-Za-z0-9_-]{8}\\.css', '.css'), ':[0-9]+|line [0-9]+|0x[0-9a-fA-F]+|[0-9a-fA-F]{8,}|[0-9]{6,}', ''), + _rawFrames + ) AS _topFrames, + if(length(_topFrames) > 0, _topFrames[1], '') AS _topFrame, + arrayStringConcat(_topFrames, '\n') AS _fpFrames, + -- JSON detection for the message signature below. + isValidJSON(_msgText) AS _isJson, + _isJson AND JSONType(_msgText) = 'Object' AS _isJsonObj, + -- General, KEY-NAME-AGNOSTIC canonical signature: iterate ALL top-level + -- keys, redact volatile tokens (long hex / numbers) in each raw value, then + -- sort by "key=value" so key order & whitespace don't matter. No assumption + -- about which keys exist — works for any producer's JSON shape. (Nested + -- objects are hashed as their raw substring; only top-level is canonicalized.) + arrayStringConcat( + arraySort( + arrayMap( + kv -> concat(kv.1, '=', replaceRegexpAll(kv.2, '[0-9a-fA-F]{8,}|[0-9]+', '#')), + JSONExtractKeysAndValuesRaw(_msgText) + ) + ), + '|' + ) AS _jsonSig, + -- The message signature is folded in ALWAYS, not only when there are no + -- frames. Bundled runtimes minify every module into one file, so the top + -- three frames of a Worker error are `toDatabaseError (worker.js)` for + -- every failing query alike: on frames alone, 25 distinct DatabaseError + -- bugs (316k occurrences) collapse into a single issue. The signature + -- restores that discrimination, and it cannot reinflate cardinality the + -- way a raw prefix would because everything variable is redacted first: + -- emails, URL origins, home directories, query strings, quoted values, + -- then ids and every digit run. See MSG_TEXT_REDACTIONS for the order, + -- what is deliberately kept, and the one residual it cannot reach. + multiIf( + _isJsonObj, _jsonSig, + substringUTF8( + replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(substringUTF8(_msgText, 1, 400), '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}', 'EMAIL'), 'https?://[^/ )"]+', ''), '/(Users|home)/[^/ ]+', '/~'), '[?][A-Za-z0-9_]+=[^ )"]*', '?#'), '\'[^\' ]*/[^\' ]*\'|\'[^\' ]{25,}\'', '\'#\''), '"[^" ]*/[^" ]*"|"[^" ]{25,}"', '"#"'), '`[^` ]*/[^` ]*`|`[^` ]{25,}`', '`#`'), '[0-9a-fA-F-]{6,}|[0-9]+', '#'), + 1, 120 + ) + ) AS _msgSig, + -- Display-only, best-effort human label (decoupled from the fingerprint: + -- many labels may map to one hash). The broad key list here is a DISPLAY + -- heuristic only; the fingerprint above makes no key-name assumption. + multiIf( + JSONExtractString(_msgText, 'title') != '', JSONExtractString(_msgText, 'title'), + JSONExtractString(_msgText, 'message') != '', JSONExtractString(_msgText, 'message'), + JSONExtractString(_msgText, 'error') != '', JSONExtractString(_msgText, 'error'), + JSONExtractString(_msgText, '_tag') != '', JSONExtractString(_msgText, '_tag'), + JSONExtractString(_msgText, 'reason') != '', JSONExtractString(_msgText, 'reason'), + JSONExtractString(_msgText, 'name') != '', JSONExtractString(_msgText, 'name'), + JSONExtractString(_msgText, 'type') != '', extract(JSONExtractString(_msgText, 'type'), '([^/]+)$'), + 'JSON error' + ) AS _jsonLabel, + multiIf( + _msgText = '', 'Unknown Error', + position(_msgText, '{ readonly') = 1 OR position(_msgText, '└─') > 0, + if( + extract(_msgText, 'readonly (\\w+)') != '', + concat('Schema parse error: ', extract(_msgText, 'readonly (\\w+)')), + 'Schema parse error' + ), + _isJsonObj OR position(_msgText, '[') = 1, _jsonLabel, + left(_msgText, multiIf( + position(_msgText, ': ') > 3, toInt64(position(_msgText, ': ')) - 1, + position(_msgText, ' (') > 3, toInt64(position(_msgText, ' (')) - 1, + position(_msgText, '\n') > 3, toInt64(position(_msgText, '\n')) - 1, + least(toInt64(length(_msgText)), 150) + )) + ) AS _statusLabel, + if(_exType != '', _exType, _statusLabel) AS _errorLabel, + -- Both semconv spellings; the current key wins when both are present. + toUInt16OrZero( + if( + SpanAttributes['http.response.status_code'] != '', + SpanAttributes['http.response.status_code'], + SpanAttributes['http.status_code'] + ) + ) AS _httpStatus + SELECT + OrgId, + toDateTime(Timestamp) AS Timestamp, + TraceId, + SpanId, + ParentSpanId, + ServiceName, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv, + _exType AS ExceptionType, + _exMsg AS ExceptionMessage, + _exStack AS ExceptionStacktrace, + _topFrame AS TopFrame, + cityHash64(OrgId, ServiceName, _exType, _fpFrames, _msgSig) AS FingerprintHash, + StatusMessage, + Duration, + _errorLabel AS ErrorLabel, + ResourceAttributes['service.version'] AS ServiceVersion + FROM traces + WHERE StatusCode = 'Error' + -- Client-side runtimes (notably the native Cloudflare Workers + -- observability) mark ANY non-2xx fetch span as Error, so 404s from bot + -- traffic arrived here as unlabelled "Unknown Error" issues. Drop a + -- span only when all hold: 4xx, no exception event, no exception.type + -- attribute, and no error.type beyond the status code itself (HTTP + -- semconv sets error.type to the bare status on a non-2xx response, + -- which carries no exception). 5xx and anything carrying a real + -- exception still count, and SpanKind is deliberately not consulted — + -- these are Client spans. + AND NOT ( + _httpStatus >= 400 AND _httpStatus < 500 + AND _ei = 0 + AND SpanAttributes['exception.type'] = '' + AND (SpanAttributes['error.type'] = '' OR SpanAttributes['error.type'] = toString(_httpStatus)) + ); + +CREATE MATERIALIZED VIEW IF NOT EXISTS error_fingerprints_minutely_mv TO error_fingerprints_minutely AS +SELECT + OrgId, + toStartOfMinute(Timestamp) AS Minute, + FingerprintHash, + anyLast(ServiceName) AS ServiceName, + anyLast(ExceptionType) AS ExceptionType, + anyLast(ExceptionMessage) AS ExceptionMessage, + anyLast(ErrorLabel) AS ErrorLabel, + anyLast(TopFrame) AS TopFrame, + count() AS OccurrenceCount, + min(Timestamp) AS FirstSeen, + max(Timestamp) AS LastSeen, + -- Distinct builds, not a sample: see ServiceVersions on the datasource. + groupUniqArray(ServiceVersion) AS ServiceVersions + FROM error_events + GROUP BY OrgId, Minute, FingerprintHash; + +CREATE MATERIALIZED VIEW IF NOT EXISTS identity_links_mv TO identity_links AS +SELECT + OrgId, + VisitorId, + UserId, + StartTime AS FirstSeen + FROM session_replays + WHERE VisitorId != '' AND UserId != ''; + +CREATE MATERIALIZED VIEW IF NOT EXISTS log_attribute_keys_mv TO attribute_keys_hourly AS +SELECT + OrgId, + toStartOfHour(toDateTime(Timestamp)) AS Hour, + arrayJoin(mapKeys(LogAttributes)) AS AttributeKey, + 'log' AS AttributeScope, + count() AS UsageCount + FROM logs + WHERE LogAttributes != map() + GROUP BY OrgId, Hour, AttributeKey, AttributeScope; + +CREATE MATERIALIZED VIEW IF NOT EXISTS log_attribute_values_mv TO attribute_values_hourly AS +SELECT + OrgId, + toStartOfHour(toDateTime(Timestamp)) AS Hour, + AttributeKey, + AttributeValue, + 'log' AS AttributeScope, + count() AS UsageCount + FROM logs + ARRAY JOIN + mapKeys(LogAttributes) AS AttributeKey, + mapValues(LogAttributes) AS AttributeValue + WHERE AttributeValue != '' + AND length(AttributeValue) <= 128 + AND NOT (length(AttributeValue) > 4 AND match(AttributeValue, '^[0-9]+([.][0-9]+)?$')) + AND NOT match(AttributeKey, '(_id|[.]id|Id|_ns)$') + AND AttributeKey NOT LIKE 'http.request.header.%' + AND AttributeKey NOT LIKE 'http.response.header.%' + GROUP BY OrgId, Hour, AttributeKey, AttributeValue, AttributeScope; + +CREATE MATERIALIZED VIEW IF NOT EXISTS logs_aggregates_hourly_mv TO logs_aggregates_hourly AS +SELECT + OrgId, + toStartOfHour(TimestampTime) AS Hour, + ServiceName, + SeverityText, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv, + count() AS Count, + sum(length(Body) + 200) AS SizeBytes, + ResourceAttributes['service.namespace'] AS ServiceNamespace + FROM logs + GROUP BY OrgId, Hour, ServiceName, SeverityText, DeploymentEnv, ServiceNamespace; + +CREATE MATERIALIZED VIEW IF NOT EXISTS metric_attribute_keys_mv TO attribute_keys_hourly AS +SELECT + OrgId, + toStartOfHour(toDateTime(TimeUnix)) AS Hour, + arrayJoin(mapKeys(Attributes)) AS AttributeKey, + 'metric' AS AttributeScope, + count() AS UsageCount + FROM metrics_sum + WHERE Attributes != map() + GROUP BY OrgId, Hour, AttributeKey, AttributeScope; + +CREATE MATERIALIZED VIEW IF NOT EXISTS metric_attribute_values_mv TO attribute_values_hourly AS +SELECT + OrgId, + toStartOfHour(toDateTime(TimeUnix)) AS Hour, + AttributeKey, + AttributeValue, + 'metric' AS AttributeScope, + count() AS UsageCount + FROM metrics_sum + ARRAY JOIN + mapKeys(Attributes) AS AttributeKey, + mapValues(Attributes) AS AttributeValue + WHERE AttributeValue != '' + AND length(AttributeValue) <= 128 + AND NOT (length(AttributeValue) > 4 AND match(AttributeValue, '^[0-9]+([.][0-9]+)?$')) + AND NOT match(AttributeKey, '(_id|[.]id|Id|_ns)$') + AND AttributeKey NOT LIKE 'http.request.header.%' + AND AttributeKey NOT LIKE 'http.response.header.%' + GROUP BY OrgId, Hour, AttributeKey, AttributeValue, AttributeScope; + +CREATE MATERIALIZED VIEW IF NOT EXISTS metric_catalog_exp_histogram_mv TO metric_catalog AS +SELECT + OrgId, + toStartOfHour(toDateTime(TimeUnix)) AS Hour, + 'exponential_histogram' AS MetricType, + ServiceName, + MetricName, + anyLast(MetricDescription) AS MetricDescription, + anyLast(MetricUnit) AS MetricUnit, + toUInt8(0) AS IsMonotonic, + count() AS DataPointCount, + min(toDateTime(TimeUnix)) AS FirstSeen, + max(toDateTime(TimeUnix)) AS LastSeen + FROM metrics_exponential_histogram + GROUP BY OrgId, Hour, MetricType, ServiceName, MetricName; + +CREATE MATERIALIZED VIEW IF NOT EXISTS metric_catalog_gauge_mv TO metric_catalog AS +SELECT + OrgId, + toStartOfHour(toDateTime(TimeUnix)) AS Hour, + 'gauge' AS MetricType, + ServiceName, + MetricName, + anyLast(MetricDescription) AS MetricDescription, + anyLast(MetricUnit) AS MetricUnit, + toUInt8(0) AS IsMonotonic, + count() AS DataPointCount, + min(toDateTime(TimeUnix)) AS FirstSeen, + max(toDateTime(TimeUnix)) AS LastSeen + FROM metrics_gauge + GROUP BY OrgId, Hour, MetricType, ServiceName, MetricName; + +CREATE MATERIALIZED VIEW IF NOT EXISTS metric_catalog_histogram_mv TO metric_catalog AS +SELECT + OrgId, + toStartOfHour(toDateTime(TimeUnix)) AS Hour, + 'histogram' AS MetricType, + ServiceName, + MetricName, + anyLast(MetricDescription) AS MetricDescription, + anyLast(MetricUnit) AS MetricUnit, + toUInt8(0) AS IsMonotonic, + count() AS DataPointCount, + min(toDateTime(TimeUnix)) AS FirstSeen, + max(toDateTime(TimeUnix)) AS LastSeen + FROM metrics_histogram + GROUP BY OrgId, Hour, MetricType, ServiceName, MetricName; + +CREATE MATERIALIZED VIEW IF NOT EXISTS metric_catalog_sum_mv TO metric_catalog AS +SELECT + OrgId, + toStartOfHour(toDateTime(TimeUnix)) AS Hour, + 'sum' AS MetricType, + ServiceName, + MetricName, + anyLast(MetricDescription) AS MetricDescription, + anyLast(MetricUnit) AS MetricUnit, + anyLast(toUInt8(IsMonotonic)) AS IsMonotonic, + count() AS DataPointCount, + min(toDateTime(TimeUnix)) AS FirstSeen, + max(toDateTime(TimeUnix)) AS LastSeen + FROM metrics_sum + GROUP BY OrgId, Hour, MetricType, ServiceName, MetricName; + +CREATE MATERIALIZED VIEW IF NOT EXISTS product_events_mv TO product_events AS +SELECT + OrgId, + Timestamp, + 'browser' AS Source, + SessionId, + Seq, + VisitorId, + UserId, + GroupId, + Type AS Kind, + if(Type = 'navigation', '$pageview', Message) AS EventName, + domain(Url) AS Host, + path(Url) AS PagePath, + Url, + '' AS ServiceName, + Attributes, + '' AS TraceId, + '' AS SpanId + FROM session_events + WHERE Type IN ('navigation', 'custom'); + +CREATE MATERIALIZED VIEW IF NOT EXISTS product_events_traces_mv TO product_events AS +SELECT + OrgId, + Timestamp, + 'trace' AS Source, + SpanAttributes['session.id'] AS SessionId, + 0 AS Seq, + SpanAttributes['maple.product_event.visitor_id'] AS VisitorId, + SpanAttributes['maple.product_event.user_id'] AS UserId, + SpanAttributes['maple.product_event.group_id'] AS GroupId, + 'custom' AS Kind, + SpanAttributes['maple.product_event.name'] AS EventName, + domain(SpanAttributes['maple.product_event.url']) AS Host, + path(SpanAttributes['maple.product_event.url']) AS PagePath, + SpanAttributes['maple.product_event.url'] AS Url, + ServiceName, + mapUpdate( + CAST( + mapFilter( + (k, v) -> NOT startsWith(k, 'maple.product_event.') + AND ( + NOT has(mapKeys(SpanAttributes), 'maple.product_event.include') + OR has( + arrayMap( + key -> trimBoth(key), + splitByChar(',', SpanAttributes['maple.product_event.include']) + ), + k + ) + ), + SpanAttributes + ), + 'Map(String, String)' + ), + mapApply( + (k, v) -> (substring(k, 26), v), + mapFilter((k, v) -> startsWith(k, 'maple.product_event.prop.'), SpanAttributes) + ) + ) AS Attributes, + TraceId, + SpanId + FROM traces + WHERE SpanAttributes['maple.product_event.name'] != ''; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_external_edges_hourly_mv TO service_external_edges_hourly AS +SELECT + OrgId, + toStartOfHour(toDateTime(Timestamp)) AS Hour, + ServiceName, + multiIf( + coalesce(nullIf(SpanAttributes['messaging.destination.name'], ''), SpanAttributes['messaging.destination']) != '' OR SpanAttributes['messaging.system'] != '', 'messaging', + SpanAttributes['rpc.service'] != '' OR SpanAttributes['rpc.system'] != '', 'rpc', + 'http' + ) AS TargetType, + multiIf( + coalesce(nullIf(SpanAttributes['messaging.destination.name'], ''), SpanAttributes['messaging.destination']) != '' OR SpanAttributes['messaging.system'] != '', SpanAttributes['messaging.system'], + SpanAttributes['rpc.service'] != '' OR SpanAttributes['rpc.system'] != '', SpanAttributes['rpc.system'], + '' + ) AS TargetSystem, + multiIf( + coalesce(nullIf(SpanAttributes['messaging.destination.name'], ''), SpanAttributes['messaging.destination']) != '' OR SpanAttributes['messaging.system'] != '', + if(coalesce(nullIf(SpanAttributes['messaging.destination.name'], ''), SpanAttributes['messaging.destination']) != '', coalesce(nullIf(SpanAttributes['messaging.destination.name'], ''), SpanAttributes['messaging.destination']), SpanAttributes['messaging.system']), + SpanAttributes['rpc.service'] != '' OR SpanAttributes['rpc.system'] != '', + if(SpanAttributes['rpc.service'] != '', SpanAttributes['rpc.service'], SpanAttributes['rpc.system']), + if(SpanAttributes['server.address'] != '', + SpanAttributes['server.address'], + if(SpanAttributes['http.host'] != '', + SpanAttributes['http.host'], + SpanAttributes['url.authority'])) + ) AS TargetName, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv, + count() AS CallCount, + countIf(StatusCode = 'Error') AS ErrorCount, + sum(Duration / 1000000) AS DurationSumMs, + max(Duration / 1000000) AS MaxDurationMs, + sum(SampleRate) AS SampleRateSum, + quantilesTDigestWeightedState(0.5, 0.95)(Duration, toUInt32(greatest(SampleRate, 1.0))) AS DurationQuantiles + FROM traces + WHERE SpanKind IN ('Client', 'Producer') + AND SpanAttributes['db.system.name'] = '' + AND ServiceName != '' + AND ( + SpanAttributes['server.address'] != '' + OR SpanAttributes['http.host'] != '' + OR SpanAttributes['url.authority'] != '' + OR coalesce(nullIf(SpanAttributes['messaging.destination.name'], ''), SpanAttributes['messaging.destination']) != '' + OR SpanAttributes['messaging.system'] != '' + OR SpanAttributes['rpc.service'] != '' + OR SpanAttributes['rpc.system'] != '' + ) + GROUP BY OrgId, Hour, ServiceName, TargetType, TargetSystem, TargetName, DeploymentEnv + HAVING TargetName != ''; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_map_children_mv TO service_map_children AS +SELECT + OrgId, + toDateTime(Timestamp) AS Timestamp, + TraceId, + ParentSpanId, + ServiceName, + SpanKind, + Duration, + StatusCode, + TraceState, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv + FROM traces + WHERE SpanKind IN ('Server', 'Consumer') + AND ParentSpanId != ''; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_map_db_edges_hourly_mv TO service_map_db_edges_hourly AS +SELECT + OrgId, + toStartOfHour(toDateTime(Timestamp)) AS Hour, + ServiceName, + coalesce(nullIf(SpanAttributes['db.system.name'], ''), SpanAttributes['db.system']) AS DbSystem, + if(match(coalesce(nullIf(SpanAttributes['db.namespace'], ''), nullIf(SpanAttributes['db.name'], ''), nullIf(SpanAttributes['server.address'], ''), SpanAttributes['net.peer.name']), '^([0-9a-fA-F]{32}|.*[.]hyperdrive[.]local)$'), 'hyperdrive', coalesce(nullIf(SpanAttributes['db.namespace'], ''), nullIf(SpanAttributes['db.name'], ''), nullIf(SpanAttributes['server.address'], ''), SpanAttributes['net.peer.name'])) AS DbNamespace, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv, + count() AS CallCount, + countIf(StatusCode = 'Error') AS ErrorCount, + sum(Duration / 1000000) AS DurationSumMs, + max(Duration / 1000000) AS MaxDurationMs, + countIf(TraceState LIKE '%th:%') AS SampledSpanCount, + countIf(TraceState = '' OR TraceState NOT LIKE '%th:%') AS UnsampledSpanCount, + sum(SampleRate) AS SampleRateSum, + quantilesTDigestWeightedState(0.5, 0.95)(Duration, toUInt32(greatest(SampleRate, 1.0))) AS DurationQuantiles + FROM traces + WHERE SpanKind IN ('Client', 'Producer') + AND coalesce(nullIf(SpanAttributes['db.system.name'], ''), SpanAttributes['db.system']) != '' + AND ServiceName != '' + GROUP BY OrgId, Hour, ServiceName, DbSystem, DbNamespace, DeploymentEnv; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_map_db_query_shapes_hourly_mv TO service_map_db_query_shapes_hourly AS +SELECT + OrgId, + toStartOfHour(toDateTime(Timestamp)) AS Hour, + ServiceName, + coalesce(nullIf(SpanAttributes['db.system.name'], ''), SpanAttributes['db.system']) AS DbSystem, + if(match(coalesce(nullIf(SpanAttributes['db.namespace'], ''), nullIf(SpanAttributes['db.name'], ''), nullIf(SpanAttributes['server.address'], ''), SpanAttributes['net.peer.name']), '^([0-9a-fA-F]{32}|.*[.]hyperdrive[.]local)$'), 'hyperdrive', coalesce(nullIf(SpanAttributes['db.namespace'], ''), nullIf(SpanAttributes['db.name'], ''), nullIf(SpanAttributes['server.address'], ''), SpanAttributes['net.peer.name'])) AS DbNamespace, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv, + coalesce( + nullIf(SpanAttributes['db.query.fingerprint'], ''), + nullIf(SpanAttributes['db.statement.fingerprint'], ''), + nullIf(if(coalesce(nullIf(SpanAttributes['db.query.text'], ''), SpanAttributes['db.statement']) != '', toString(cityHash64(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(lower(coalesce(nullIf(SpanAttributes['db.query.text'], ''), SpanAttributes['db.statement'])), '\'[^\']*\'', '?'), '\\bin\\s*\\([^)]*\\)', 'in (?)'), '[0-9]+(\\.[0-9]+)?', '?'), '\\s+', ' '), '^\\s+|\\s+$', ''))), ''), ''), + toString(cityHash64(coalesce( + nullIf(SpanAttributes['db.query.summary'], ''), + nullIf(if(SpanAttributes['db.operation.name'] != '', trimBoth(concat(SpanAttributes['db.operation.name'], if(coalesce(nullIf(SpanAttributes['db.collection.name'], ''), SpanAttributes['db.namespace']) != '', concat(' ', coalesce(nullIf(SpanAttributes['db.collection.name'], ''), SpanAttributes['db.namespace'])), ''))), ''), ''), + nullIf(if(coalesce(nullIf(SpanAttributes['db.query.text'], ''), SpanAttributes['db.statement']) != '', trimBoth(concat(upper(extract(coalesce(nullIf(SpanAttributes['db.query.text'], ''), SpanAttributes['db.statement']), '^\\s*(\\w+)')), if(extract(coalesce(nullIf(SpanAttributes['db.query.text'], ''), SpanAttributes['db.statement']), '(?i)(?:from|into|update|join|table)\\s+\\W?([\\w.]+)') != '', concat(' ', extract(coalesce(nullIf(SpanAttributes['db.query.text'], ''), SpanAttributes['db.statement']), '(?i)(?:from|into|update|join|table)\\s+\\W?([\\w.]+)')), ''))), ''), ''), + nullIf(SpanAttributes['query.context'], ''), + nullIf(SpanAttributes['db.operation.name'], ''), + nullIf(SpanAttributes['db.operation'], ''), + SpanName +))) +) AS QueryKey, + any(substring(coalesce( + nullIf(SpanAttributes['db.query.summary'], ''), + nullIf(if(SpanAttributes['db.operation.name'] != '', trimBoth(concat(SpanAttributes['db.operation.name'], if(coalesce(nullIf(SpanAttributes['db.collection.name'], ''), SpanAttributes['db.namespace']) != '', concat(' ', coalesce(nullIf(SpanAttributes['db.collection.name'], ''), SpanAttributes['db.namespace'])), ''))), ''), ''), + nullIf(if(coalesce(nullIf(SpanAttributes['db.query.text'], ''), SpanAttributes['db.statement']) != '', trimBoth(concat(upper(extract(coalesce(nullIf(SpanAttributes['db.query.text'], ''), SpanAttributes['db.statement']), '^\\s*(\\w+)')), if(extract(coalesce(nullIf(SpanAttributes['db.query.text'], ''), SpanAttributes['db.statement']), '(?i)(?:from|into|update|join|table)\\s+\\W?([\\w.]+)') != '', concat(' ', extract(coalesce(nullIf(SpanAttributes['db.query.text'], ''), SpanAttributes['db.statement']), '(?i)(?:from|into|update|join|table)\\s+\\W?([\\w.]+)')), ''))), ''), ''), + nullIf(SpanAttributes['query.context'], ''), + nullIf(SpanAttributes['db.operation.name'], ''), + nullIf(SpanAttributes['db.operation'], ''), + SpanName +), 1, 220)) AS QueryLabel, + any(substring(coalesce(nullIf(SpanAttributes['db.query.text'], ''), SpanAttributes['db.statement']), 1, 1000)) AS SampleStatement, + count() AS CallCount, + countIf(StatusCode = 'Error') AS ErrorCount, + sum(SampleRate) AS EstimatedCount, + sumIf(SampleRate, StatusCode = 'Error') AS EstimatedErrorCount, + sum(toFloat64(Duration) * SampleRate / 1000000) AS WeightedDurationSumMs, + quantilesTDigestWeightedState(0.5, 0.95)(Duration, toUInt32(greatest(SampleRate, 1.0))) AS DurationQuantiles + FROM traces + WHERE SpanKind IN ('Client', 'Producer') + AND coalesce(nullIf(SpanAttributes['db.system.name'], ''), SpanAttributes['db.system']) != '' + AND ServiceName != '' + GROUP BY OrgId, Hour, ServiceName, DbSystem, DbNamespace, DeploymentEnv, QueryKey; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_map_edges_hourly_ingest_mv TO service_map_edges_hourly AS +SELECT + OrgId, + Hour, + SourceService, + TargetService, + DeploymentEnv, + CallCount, + ErrorCount, + DurationSumMs, + MaxDurationMs, + SampledSpanCount, + UnsampledSpanCount, + SampleRateSum + FROM service_map_edges_hourly_ingest; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_map_spans_mv TO service_map_spans AS +SELECT + OrgId, + toDateTime(Timestamp) AS Timestamp, + TraceId, + SpanId, + ParentSpanId, + ServiceName, + SpanKind, + Duration, + StatusCode, + TraceState, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv + FROM traces + WHERE SpanKind IN ('Client', 'Producer', 'Server', 'Consumer'); + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_operations_hourly_mv TO service_operations_hourly AS +SELECT + OrgId, + toStartOfHour(Minute) AS Hour, + ServiceName, + DeploymentEnv, + SpanName, + sum(SpanCount) AS SpanCount, + sum(EstimatedSpanCount) AS EstimatedSpanCount, + sum(ErrorCount) AS ErrorCount, + sum(EstimatedErrorCount) AS EstimatedErrorCount, + sum(DurationSum) AS DurationSum, + quantilesTDigestMergeState(0.5, 0.95)(DurationQuantiles) AS DurationQuantiles, + sum(ClassifiedSpanCount) AS ClassifiedSpanCount, + sum(ServerSpanCount) AS ServerSpanCount, + sum(RoutedSpanCount) AS RoutedSpanCount + FROM service_operations_minutely + GROUP BY OrgId, Hour, ServiceName, DeploymentEnv, SpanName; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_operations_minutely_mv TO service_operations_minutely AS +SELECT + OrgId, + toStartOfMinute(toDateTime(Timestamp)) AS Minute, + ServiceName, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv, + if(((SpanName LIKE 'http.server %' OR SpanName IN ('GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS')) AND (SpanAttributes['http.route'] != '' OR SpanAttributes['url.path'] != '')), concat(if(SpanName LIKE 'http.server %', replaceOne(SpanName, 'http.server ', ''), SpanName), ' ', if(SpanAttributes['http.route'] != '', SpanAttributes['http.route'], SpanAttributes['url.path'])), SpanName) AS SpanName, + count() AS SpanCount, + sum(SampleRate) AS EstimatedSpanCount, + countIf(StatusCode = 'Error') AS ErrorCount, + sumIf(SampleRate, StatusCode = 'Error') AS EstimatedErrorCount, + sum(toFloat64(Duration)) AS DurationSum, + quantilesTDigestState(0.5, 0.95)(Duration) AS DurationQuantiles, + count() AS ClassifiedSpanCount, + countIf(SpanKind IN ('Server', 'Consumer')) AS ServerSpanCount, + countIf(SpanAttributes['http.route'] != '') AS RoutedSpanCount + FROM traces + GROUP BY OrgId, Minute, ServiceName, DeploymentEnv, SpanName; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_overview_hourly_mv TO service_overview_hourly AS +SELECT + OrgId, + toStartOfHour(toDateTime(Timestamp)) AS Hour, + ServiceName, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv, + ResourceAttributes['service.namespace'] AS ServiceNamespace, + ResourceAttributes['vcs.ref.head.revision'] AS CommitSha, + count() AS SpanCount, + sum(SampleRate) AS EstimatedSpanCount, + countIf(StatusCode = 'Error') AS ErrorCount, + sumIf(SampleRate, StatusCode = 'Error') AS EstimatedErrorCount, + sum(toFloat64(Duration)) AS DurationSum, + quantilesTDigestState(0.5, 0.95, 0.99)(Duration) AS DurationQuantiles, + min(toDateTime(Timestamp)) AS FirstSeen, + countIf(StatusCode != 'Error' AND Duration < 500000000) AS ApdexSatisfiedCount, + countIf(StatusCode != 'Error' AND Duration >= 500000000 AND Duration < 2000000000) AS ApdexToleratingCount + FROM traces + WHERE SpanKind IN ('Server', 'Consumer') OR ParentSpanId = '' + GROUP BY OrgId, Hour, ServiceName, DeploymentEnv, ServiceNamespace, CommitSha; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_overview_minutely_mv TO service_overview_minutely AS +SELECT + OrgId, + toStartOfMinute(toDateTime(Timestamp)) AS Minute, + ServiceName, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv, + ResourceAttributes['service.namespace'] AS ServiceNamespace, + ResourceAttributes['vcs.ref.head.revision'] AS CommitSha, + count() AS SpanCount, + sum(SampleRate) AS EstimatedSpanCount, + countIf(StatusCode = 'Error') AS ErrorCount, + sumIf(SampleRate, StatusCode = 'Error') AS EstimatedErrorCount, + sum(toFloat64(Duration)) AS DurationSum, + quantilesTDigestState(0.5, 0.95, 0.99)(Duration) AS DurationQuantiles, + min(toDateTime(Timestamp)) AS FirstSeen, + countIf(StatusCode != 'Error' AND Duration < 500000000) AS ApdexSatisfiedCount, + countIf(StatusCode != 'Error' AND Duration >= 500000000 AND Duration < 2000000000) AS ApdexToleratingCount + FROM traces + WHERE SpanKind IN ('Server', 'Consumer') OR ParentSpanId = '' + GROUP BY OrgId, Minute, ServiceName, DeploymentEnv, ServiceNamespace, CommitSha; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_overview_spans_mv TO service_overview_spans AS +SELECT + OrgId, + toDateTime(Timestamp) AS Timestamp, + ServiceName, + Duration, + StatusCode, + TraceState, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv, + ResourceAttributes['vcs.ref.head.revision'] AS CommitSha, + SampleRate, + ResourceAttributes['service.namespace'] AS ServiceNamespace + FROM traces + WHERE SpanKind IN ('Server', 'Consumer') OR ParentSpanId = ''; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_platforms_hourly_mv TO service_platforms_hourly AS +SELECT + OrgId, + toStartOfHour(toDateTime(Timestamp)) AS Hour, + ServiceName, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv, + max(ResourceAttributes['k8s.cluster.name']) AS K8sCluster, + max(ResourceAttributes['k8s.pod.name']) AS K8sPodName, + max(ResourceAttributes['k8s.deployment.name']) AS K8sDeploymentName, + max(ResourceAttributes['k8s.statefulset.name']) AS K8sStatefulSetName, + max(ResourceAttributes['k8s.daemonset.name']) AS K8sDaemonSetName, + max(ResourceAttributes['k8s.namespace.name']) AS K8sNamespaceName, + max(ResourceAttributes['cloud.platform']) AS CloudPlatform, + max(ResourceAttributes['cloud.provider']) AS CloudProvider, + max(ResourceAttributes['faas.name']) AS FaasName, + max(ResourceAttributes['maple.sdk.type']) AS MapleSdkType, + max(ResourceAttributes['process.runtime.name']) AS ProcessRuntimeName, + count() AS SpanCount + FROM traces + WHERE ServiceName != '' + GROUP BY OrgId, Hour, ServiceName, DeploymentEnv; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_usage_logs_mv TO service_usage AS +SELECT + OrgId, + ServiceName, + toStartOfHour(TimestampTime) AS Hour, + count() AS LogCount, + sum(length(Body) + 200) AS LogSizeBytes, + 0 AS TraceCount, + 0 AS TraceSizeBytes, + 0 AS SumMetricCount, + 0 AS SumMetricSizeBytes, + 0 AS GaugeMetricCount, + 0 AS GaugeMetricSizeBytes, + 0 AS HistogramMetricCount, + 0 AS HistogramMetricSizeBytes, + 0 AS ExpHistogramMetricCount, + 0 AS ExpHistogramMetricSizeBytes + FROM logs + GROUP BY OrgId, ServiceName, Hour; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_usage_metrics_exp_histogram_mv TO service_usage AS +SELECT + OrgId, + ServiceName, + toStartOfHour(toDateTime(TimeUnix)) AS Hour, + 0 AS LogCount, + 0 AS LogSizeBytes, + 0 AS TraceCount, + 0 AS TraceSizeBytes, + 0 AS SumMetricCount, + 0 AS SumMetricSizeBytes, + 0 AS GaugeMetricCount, + 0 AS GaugeMetricSizeBytes, + 0 AS HistogramMetricCount, + 0 AS HistogramMetricSizeBytes, + count() AS ExpHistogramMetricCount, + count() * 300 AS ExpHistogramMetricSizeBytes + FROM metrics_exponential_histogram + GROUP BY OrgId, ServiceName, Hour; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_usage_metrics_gauge_mv TO service_usage AS +SELECT + OrgId, + ServiceName, + toStartOfHour(toDateTime(TimeUnix)) AS Hour, + 0 AS LogCount, + 0 AS LogSizeBytes, + 0 AS TraceCount, + 0 AS TraceSizeBytes, + 0 AS SumMetricCount, + 0 AS SumMetricSizeBytes, + count() AS GaugeMetricCount, + count() * 150 AS GaugeMetricSizeBytes, + 0 AS HistogramMetricCount, + 0 AS HistogramMetricSizeBytes, + 0 AS ExpHistogramMetricCount, + 0 AS ExpHistogramMetricSizeBytes + FROM metrics_gauge + GROUP BY OrgId, ServiceName, Hour; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_usage_metrics_histogram_mv TO service_usage AS +SELECT + OrgId, + ServiceName, + toStartOfHour(toDateTime(TimeUnix)) AS Hour, + 0 AS LogCount, + 0 AS LogSizeBytes, + 0 AS TraceCount, + 0 AS TraceSizeBytes, + 0 AS SumMetricCount, + 0 AS SumMetricSizeBytes, + 0 AS GaugeMetricCount, + 0 AS GaugeMetricSizeBytes, + count() AS HistogramMetricCount, + count() * 250 AS HistogramMetricSizeBytes, + 0 AS ExpHistogramMetricCount, + 0 AS ExpHistogramMetricSizeBytes + FROM metrics_histogram + GROUP BY OrgId, ServiceName, Hour; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_usage_metrics_sum_mv TO service_usage AS +SELECT + OrgId, + ServiceName, + toStartOfHour(toDateTime(TimeUnix)) AS Hour, + 0 AS LogCount, + 0 AS LogSizeBytes, + 0 AS TraceCount, + 0 AS TraceSizeBytes, + count() AS SumMetricCount, + count() * 150 AS SumMetricSizeBytes, + 0 AS GaugeMetricCount, + 0 AS GaugeMetricSizeBytes, + 0 AS HistogramMetricCount, + 0 AS HistogramMetricSizeBytes, + 0 AS ExpHistogramMetricCount, + 0 AS ExpHistogramMetricSizeBytes + FROM metrics_sum + GROUP BY OrgId, ServiceName, Hour; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_usage_traces_mv TO service_usage AS +SELECT + OrgId, + ServiceName, + toStartOfHour(toDateTime(Timestamp)) AS Hour, + 0 AS LogCount, + 0 AS LogSizeBytes, + count() AS TraceCount, + sum(length(SpanName) + 300) AS TraceSizeBytes, + 0 AS SumMetricCount, + 0 AS SumMetricSizeBytes, + 0 AS GaugeMetricCount, + 0 AS GaugeMetricSizeBytes, + 0 AS HistogramMetricCount, + 0 AS HistogramMetricSizeBytes, + 0 AS ExpHistogramMetricCount, + 0 AS ExpHistogramMetricSizeBytes + FROM traces + GROUP BY OrgId, ServiceName, Hour; + +CREATE MATERIALIZED VIEW IF NOT EXISTS span_metrics_calls_hourly_mv TO span_metrics_calls_hourly AS +SELECT + OrgId, + toStartOfHour(toDateTime(TimeUnix)) AS Hour, + ServiceName, + MetricName, + Attributes['span.kind'] AS SpanKind, + cityHash64(mapKeys(Attributes), mapValues(Attributes)) AS AttrFingerprint, + cityHash64(mapKeys(ResourceAttributes), mapValues(ResourceAttributes)) AS ResourceFingerprint, + StartTimeUnix, + argMaxState(Value, TimeUnix) AS LastValue + FROM metrics_sum + -- 'traces.span.metrics.calls' is the name the collector actually emits: + -- spanmetricsconnector output is namespaced by the pipeline it is attached + -- to. Without it this MV matched nothing and the target sat at 0 rows since + -- it was created, while ~880k rows / 2 days of the real counter flowed past + -- into metrics_sum and every read fell back to the raw window-function scan + -- (~7s p95 -- see queries/metrics.ts). Keep this list in sync with + -- SPAN_METRICS_CALLS_NAMES on the read side. + WHERE MetricName IN ('span.metrics.calls', 'calls', 'traces.span.metrics.calls') AND IsMonotonic + GROUP BY OrgId, Hour, ServiceName, MetricName, SpanKind, AttrFingerprint, ResourceFingerprint, StartTimeUnix; + +CREATE MATERIALIZED VIEW IF NOT EXISTS trace_detail_spans_mv TO trace_detail_spans AS +SELECT + OrgId, + Timestamp, + TraceId, + SpanId, + ParentSpanId, + SpanName, + SpanKind, + ServiceName, + Duration, + StatusCode, + StatusMessage, + SpanAttributes, + ResourceAttributes + FROM traces; + +CREATE MATERIALIZED VIEW IF NOT EXISTS trace_facets_hourly_mv TO trace_facets_hourly AS +SELECT + OrgId, + toStartOfHour(Timestamp) AS Hour, + ServiceName, + SpanName, + HttpMethod, + HttpStatusCode, + DeploymentEnv, + ServiceNamespace, + HasError, + count() AS TraceCount, + min(Duration) AS DurationMin, + max(Duration) AS DurationMax, + quantilesTDigestState(0.5, 0.95)(Duration) AS DurationQuantiles + FROM trace_list_mv + GROUP BY OrgId, Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError; + +CREATE MATERIALIZED VIEW IF NOT EXISTS trace_list_mv_mv TO trace_list_mv AS +SELECT + OrgId, + TraceId, + toDateTime(Timestamp) AS Timestamp, + ServiceName, + if( + (SpanName LIKE 'http.server %' OR SpanName IN ('GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS')) + AND (SpanAttributes['http.route'] != '' OR SpanAttributes['url.path'] != ''), + concat( + if(SpanName LIKE 'http.server %', replaceOne(SpanName, 'http.server ', ''), SpanName), + ' ', + if(SpanAttributes['http.route'] != '', SpanAttributes['http.route'], SpanAttributes['url.path']) + ), + SpanName + ) AS SpanName, + SpanKind, + Duration, + StatusCode, + if(SpanAttributes['http.method'] != '', SpanAttributes['http.method'], SpanAttributes['http.request.method']) AS HttpMethod, + if(SpanAttributes['http.route'] != '', SpanAttributes['http.route'], if(SpanAttributes['url.path'] != '', SpanAttributes['url.path'], SpanAttributes['http.target'])) AS HttpRoute, + if(SpanAttributes['http.status_code'] != '', SpanAttributes['http.status_code'], SpanAttributes['http.response.status_code']) AS HttpStatusCode, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv, + toUInt8( + StatusCode = 'Error' + OR (SpanAttributes['http.status_code'] != '' AND toUInt16OrZero(SpanAttributes['http.status_code']) >= 500) + OR (SpanAttributes['http.response.status_code'] != '' AND toUInt16OrZero(SpanAttributes['http.response.status_code']) >= 500) + ) AS HasError, + TraceState, + ResourceAttributes['service.namespace'] AS ServiceNamespace + FROM traces + WHERE ParentSpanId = ''; + +CREATE MATERIALIZED VIEW IF NOT EXISTS trace_resource_attribute_keys_mv TO attribute_keys_hourly AS +SELECT + OrgId, + toStartOfHour(toDateTime(Timestamp)) AS Hour, + arrayJoin(mapKeys(ResourceAttributes)) AS AttributeKey, + 'resource' AS AttributeScope, + count() AS UsageCount + FROM traces + WHERE ResourceAttributes != map() + GROUP BY OrgId, Hour, AttributeKey, AttributeScope; + +CREATE MATERIALIZED VIEW IF NOT EXISTS trace_resource_attribute_values_mv TO attribute_values_hourly AS +SELECT + OrgId, + toStartOfHour(toDateTime(Timestamp)) AS Hour, + AttributeKey, + AttributeValue, + 'resource' AS AttributeScope, + count() AS UsageCount + FROM traces + ARRAY JOIN + mapKeys(ResourceAttributes) AS AttributeKey, + mapValues(ResourceAttributes) AS AttributeValue + WHERE AttributeValue != '' + AND length(AttributeValue) <= 128 + AND NOT (length(AttributeValue) > 4 AND match(AttributeValue, '^[0-9]+([.][0-9]+)?$')) + AND NOT match(AttributeKey, '(_id|[.]id|Id|_ns)$') + AND AttributeKey NOT LIKE 'http.request.header.%' + AND AttributeKey NOT LIKE 'http.response.header.%' + GROUP BY OrgId, Hour, AttributeKey, AttributeValue, AttributeScope; + +CREATE MATERIALIZED VIEW IF NOT EXISTS trace_span_attribute_keys_mv TO attribute_keys_hourly AS +SELECT + OrgId, + toStartOfHour(toDateTime(Timestamp)) AS Hour, + arrayJoin(mapKeys(SpanAttributes)) AS AttributeKey, + 'span' AS AttributeScope, + count() AS UsageCount + FROM traces + WHERE SpanAttributes != map() + GROUP BY OrgId, Hour, AttributeKey, AttributeScope; + +CREATE MATERIALIZED VIEW IF NOT EXISTS trace_span_attribute_values_mv TO attribute_values_hourly AS +SELECT + OrgId, + toStartOfHour(toDateTime(Timestamp)) AS Hour, + AttributeKey, + AttributeValue, + 'span' AS AttributeScope, + count() AS UsageCount + FROM traces + ARRAY JOIN + mapKeys(SpanAttributes) AS AttributeKey, + mapValues(SpanAttributes) AS AttributeValue + WHERE AttributeValue != '' + AND length(AttributeValue) <= 128 + AND NOT (length(AttributeValue) > 4 AND match(AttributeValue, '^[0-9]+([.][0-9]+)?$')) + AND NOT match(AttributeKey, '(_id|[.]id|Id|_ns)$') + AND AttributeKey NOT LIKE 'http.request.header.%' + AND AttributeKey NOT LIKE 'http.response.header.%' + GROUP BY OrgId, Hour, AttributeKey, AttributeValue, AttributeScope; + +CREATE MATERIALIZED VIEW IF NOT EXISTS traces_aggregates_hourly_mv TO traces_aggregates_hourly AS +SELECT + OrgId, + toStartOfHour(toDateTime(Timestamp)) AS Hour, + ServiceName, + SpanName, + SpanKind, + StatusCode, + IsEntryPoint, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv, + sum(SampleRate) AS WeightedCount, + sum(toFloat64(Duration) * SampleRate) AS WeightedDurationSum, + sumIf(SampleRate, StatusCode = 'Error') AS WeightedErrorCount, + quantilesTDigestWeightedState(0.5, 0.95, 0.99)(Duration, toUInt32(SampleRate)) AS DurationQuantiles, + min(Duration) AS DurationMin, + max(Duration) AS DurationMax + FROM traces + GROUP BY OrgId, Hour, ServiceName, SpanName, SpanKind, StatusCode, IsEntryPoint, DeploymentEnv; diff --git a/apps/cli/src/server/schema/local-schema.sql b/apps/cli/src/server/schema/local-schema.sql index 84d0a482ba..8e1bef6d64 100644 --- a/apps/cli/src/server/schema/local-schema.sql +++ b/apps/cli/src/server/schema/local-schema.sql @@ -1,7 +1,7 @@ -- This file is generated by scripts/generate-clickhouse-schema-sql.ts -- Do not edit manually. Run `bun run clickhouse:schema` to regenerate. --- projectRevision: 455ff9a387c6e0b10a3083278c020d73ea622f1e30a106fb833bcb2262a94d6f --- localSchemaVersion: 23 +-- projectRevision: eee23295aa263ed5dce623418acbe3cdffac84bbea15570e3ac20b4e1d3b9b50 +-- localSchemaVersion: 24 CREATE TABLE IF NOT EXISTS ai_crawler_requests ( OrgId LowCardinality(String), @@ -858,6 +858,26 @@ PARTITION BY toDate(Timestamp) ORDER BY (OrgId, TraceId, SpanId) TTL toDate(Timestamp) + INTERVAL 30 DAY; +CREATE TABLE IF NOT EXISTS trace_facets_hourly ( + OrgId LowCardinality(String), + Hour DateTime, + ServiceName LowCardinality(String), + SpanName String, + HttpMethod LowCardinality(String), + HttpStatusCode LowCardinality(String), + DeploymentEnv LowCardinality(String), + ServiceNamespace LowCardinality(String), + HasError UInt8, + TraceCount SimpleAggregateFunction(sum, UInt64), + DurationMin SimpleAggregateFunction(min, UInt64), + DurationMax SimpleAggregateFunction(max, UInt64), + DurationQuantiles AggregateFunction(quantilesTDigest(0.5, 0.95), UInt64) +) +ENGINE = AggregatingMergeTree +PARTITION BY toYYYYMM(Hour) +ORDER BY (OrgId, Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError) +TTL toDate(Hour) + INTERVAL 30 DAY; + CREATE TABLE IF NOT EXISTS trace_list_mv ( OrgId LowCardinality(String), TraceId String, @@ -1982,6 +2002,24 @@ SELECT ResourceAttributes FROM traces; +CREATE MATERIALIZED VIEW IF NOT EXISTS trace_facets_hourly_mv TO trace_facets_hourly AS +SELECT + OrgId, + toStartOfHour(Timestamp) AS Hour, + ServiceName, + SpanName, + HttpMethod, + HttpStatusCode, + DeploymentEnv, + ServiceNamespace, + HasError, + count() AS TraceCount, + min(Duration) AS DurationMin, + max(Duration) AS DurationMax, + quantilesTDigestState(0.5, 0.95)(Duration) AS DurationQuantiles + FROM trace_list_mv + GROUP BY OrgId, Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError; + CREATE MATERIALIZED VIEW IF NOT EXISTS trace_list_mv_mv TO trace_list_mv AS SELECT OrgId, diff --git a/apps/cli/test/local-store-migrations.test.ts b/apps/cli/test/local-store-migrations.test.ts index 5dfefcb099..a1c7e73509 100644 --- a/apps/cli/test/local-store-migrations.test.ts +++ b/apps/cli/test/local-store-migrations.test.ts @@ -27,6 +27,7 @@ import { LOCAL_SCHEMA_V21, LOCAL_SCHEMA_V22, LOCAL_SCHEMA_V23, + LOCAL_SCHEMA_V24, SCHEMA_DIGEST, SCHEMA_FINGERPRINT, } from "../src/server/schema-identity" @@ -91,16 +92,16 @@ const [v10ToV11ProductEventsModule] = localStoreMigrations.filter( ) describe("current local schema identity", () => { - it("matches the generated v23 revision and keeps the issue-297 identity frozen", () => { - expect(SCHEMA_FINGERPRINT).toBe("03160af2211aeeed") - expect(SCHEMA_DIGEST).toBe("03160af2211aeeed50bac5750a0ab2148fc32eb18dcf3b500f3e99cfc5752530") + it("matches the generated v24 revision and keeps the issue-297 identity frozen", () => { + expect(SCHEMA_FINGERPRINT).toBe("b59c68cd72efa658") + expect(SCHEMA_DIGEST).toBe("b59c68cd72efa658d9c5cf602aac7b6cba32947db3d939c03f407c03865be778") expect(ISSUE_297_TARGET_SCHEMA_PROJECT_REVISION).toBe( "506bc745f7a7eca202ec905a6403a6815e86413faf0cd3cbbf73881023edce91", ) expect(CURRENT_SCHEMA_PROJECT_REVISION).toMatch(/^[0-9a-f]{64}$/) expect(LOCAL_SCHEMA_MANIFEST.objects.length).toBeGreaterThan(60) - expect(CURRENT_LOCAL_SCHEMA.version).toBe(23) - expect(CURRENT_LOCAL_SCHEMA).toEqual(LOCAL_SCHEMA_V23) + expect(CURRENT_LOCAL_SCHEMA.version).toBe(24) + expect(CURRENT_LOCAL_SCHEMA).toEqual(LOCAL_SCHEMA_V24) const logs = LOCAL_SCHEMA_MANIFEST.objects.find((object) => object.name === "logs") expect(logs?.columns.some((column) => column.name.startsWith("idx_"))).toBe(false) expect(logs?.indexes).toContain("idx_lower_body") @@ -169,7 +170,7 @@ describe("current local schema identity", () => { // differs solely through those definitions. v9 removes `error_spans` and // its view; v11 replaces `web_events` with `product_events` and adds // `identity_links`; v17 adds `audit_log`, which local mode creates but - // never writes; v23 adds `ai_crawler_requests` and its view. Asserted as an exact set difference rather than a + // never writes; v23 adds `ai_crawler_requests` and its view; v24 adds `trace_facets_hourly` and its view. Asserted as an exact set difference rather than a // relaxed check, so a future edge still cannot add or drop an object // unnoticed. expect([...v5Names].filter((name) => !currentNames.has(name))).toEqual([ @@ -189,6 +190,8 @@ describe("current local schema identity", () => { "product_events", "product_events_mv", "product_events_traces_mv", + "trace_facets_hourly", + "trace_facets_hourly_mv", ]) const errorEventsView = LOCAL_SCHEMA_MANIFEST.objects.find( (object) => object.name === "error_events_mv", @@ -292,6 +295,8 @@ describe("current local schema identity", () => { "ai_trace_index_mv", "audit_log", "product_events_traces_mv", + "trace_facets_hourly", + "trace_facets_hourly_mv", ]) expect([...v13Names].filter((name) => !currentSchemaNames.has(name))).toEqual([]) const aiTraceIndex = LOCAL_SCHEMA_MANIFEST.objects.find((object) => object.name === "ai_trace_index") @@ -346,6 +351,7 @@ describe("local migration registry", () => { "local-0020-to-0021-ai-trace-index-list-columns", "local-0021-to-0022-ai-trace-index-tool-detail-columns", "local-0022-to-0023-ai-crawler-requests", + "local-0023-to-0024-trace-facets-hourly", ]) expect(chain[0]?.from.fingerprint).toBe(LEGACY_SCHEMA_FINGERPRINT) expect(chain[0]?.to).toEqual(LOCAL_SCHEMA_V1) @@ -392,7 +398,7 @@ describe("local migration registry", () => { // One past the current tip — bump alongside LOCAL_SCHEMA_VERSION, or this // stops testing the future-store guard and starts testing the // unknown-fingerprint one. - { ...CURRENT_LOCAL_SCHEMA, version: 24, fingerprint: "future", digest: SCHEMA_DIGEST }, + { ...CURRENT_LOCAL_SCHEMA, version: 25, fingerprint: "future", digest: SCHEMA_DIGEST }, CURRENT_LOCAL_SCHEMA, ), ).toThrow(/newer than this build/) @@ -534,6 +540,7 @@ describe("local migration registry", () => { "local-0020-to-0021-ai-trace-index-list-columns", "local-0021-to-0022-ai-trace-index-tool-detail-columns", "local-0022-to-0023-ai-crawler-requests", + "local-0023-to-0024-trace-facets-hourly", ] const versions = (id: string) => { const match = /^local-(\d{4})-to-(\d{4})-[a-z0-9]+(-[a-z0-9]+)*$/.exec(id) @@ -1544,6 +1551,7 @@ describe("v10 -> v11 product events module", () => { "local-0020-to-0021-ai-trace-index-list-columns", "local-0021-to-0022-ai-trace-index-tool-detail-columns", "local-0022-to-0023-ai-crawler-requests", + "local-0023-to-0024-trace-facets-hourly", ]) expect(chain[0]?.to).toEqual(LOCAL_SCHEMA_V11) // The dropped table is declared, and the backfilled ones say what they diff --git a/apps/cli/test/native-local-store-migration.sh b/apps/cli/test/native-local-store-migration.sh index 4458a4c306..012221a024 100755 --- a/apps/cli/test/native-local-store-migration.sh +++ b/apps/cli/test/native-local-store-migration.sh @@ -142,7 +142,7 @@ grep -q "local store migrated" "$ROOT/migrate.out" || fail "native migration did # must be bumped in lockstep with LOCAL_SCHEMA_VERSION and the matching # LOCAL_SCHEMA_V.fingerprint in apps/cli/src/server/schema-identity.ts; # leaving it on the previous version is what makes this step fail after a bump. -jq -e '.formatVersion == 2 and .activation == "active" and .schemaVersion == 23 and .schema == "03160af2211aeeed"' \ +jq -e '.formatVersion == 2 and .activation == "active" and .schemaVersion == 24 and .schema == "b59c68cd72efa658"' \ "$ROOT/maple-store-version.json" >/dev/null || fail "native migration wrote the wrong active identity" step "reopening promoted store in a fresh server" diff --git a/apps/ingest/src/clickhouse_insert_mappings.rs b/apps/ingest/src/clickhouse_insert_mappings.rs index dbc09f6689..2282b2faf0 100644 --- a/apps/ingest/src/clickhouse_insert_mappings.rs +++ b/apps/ingest/src/clickhouse_insert_mappings.rs @@ -1,7 +1,7 @@ // This file is generated by scripts/generate-clickhouse-insert-mappings.ts // Do not edit manually. -pub const PROJECT_REVISION: &str = "455ff9a387c6e0b10a3083278c020d73ea622f1e30a106fb833bcb2262a94d6f"; +pub const PROJECT_REVISION: &str = "eee23295aa263ed5dce623418acbe3cdffac84bbea15570e3ac20b4e1d3b9b50"; // Gate for BYO-ClickHouse ingest readiness — the migration version, NOT the // Tinybird-coupled PROJECT_REVISION. Compared against // org_clickhouse_settings.schema_version. See @maple/domain/clickhouse diff --git a/docs/warehouse-rollups.md b/docs/warehouse-rollups.md index 5c81ecd393..3f202663a5 100644 --- a/docs/warehouse-rollups.md +++ b/docs/warehouse-rollups.md @@ -1,6 +1,6 @@ # Warehouse rollups and materialized views -We have 43 materialized views across 41 datasources. They accreted one product feature at a +We have 44 materialized views across 42 datasources. They accreted one product feature at a time, and for a long time nobody could answer "should this be an MV?" without re-deriving it from scratch. This is that answer. diff --git a/packages/backend/src/services/warehouse/trace-facets-hourly-materialization.clickhouse.e2e.test.ts b/packages/backend/src/services/warehouse/trace-facets-hourly-materialization.clickhouse.e2e.test.ts new file mode 100644 index 0000000000..604e6e4a7a --- /dev/null +++ b/packages/backend/src/services/warehouse/trace-facets-hourly-materialization.clickhouse.e2e.test.ts @@ -0,0 +1,204 @@ +// SAFETY-FILE: JSON in this test is emitted by ClickHouse before its fields are asserted. +// `trace_facets_hourly` is cascaded off `trace_list_mv`: it fires on the insert +// blocks another view writes, not on `traces`. That only works if ClickHouse +// actually chains the two views, and a rollup nobody fills reads as "no data" +// rather than as an error. So this suite proves rows: it inserts spans into +// `traces` on a database built from the real migration chain and asserts the +// rollup agrees with `trace_list_mv` group for group, then that migration 0034's +// backfill reproduces the same rollup from `trace_list_mv` alone. + +import { afterAll, assert, beforeAll, describe, it } from "@effect/vitest" +import { migrations, renderStatementFull } from "@maple/domain/clickhouse" +import { + applyRealMigrations, + clickhouseE2eEnabled, + clickhouseExec, + uniqueDatabase, +} from "./clickhouse-e2e-support" + +const database = uniqueDatabase("maple_trace_facets_e2e") +const ORG_ID = "org_trace_facets_e2e" + +// Anchored to now: `traces` carries a 30-day TTL enforced at insert. +const HOUR_MS = 3_600_000 +const BASE_MS = Math.floor((Date.now() - 3 * HOUR_MS) / HOUR_MS) * HOUR_MS + +const chDateTime = (epochMs: number): string => new Date(epochMs).toISOString().replace("T", " ").slice(0, 23) +const quote = (value: string): string => `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'` +const chMap = (attrs: Readonly>): string => + `map(${Object.entries(attrs) + .flatMap(([key, value]) => [quote(key), quote(value)]) + .join(", ")})` + +interface SeedSpan { + readonly traceId: string + readonly parentSpanId?: string + readonly name: string + readonly ms: number + readonly service: string + readonly status?: string + readonly durationNs: number + readonly attrs?: Readonly> + readonly resource?: Readonly> +} + +const PROD = { "deployment.environment.name": "production", "service.namespace": "core" } + +const BATCH_1: ReadonlyArray = [ + // Old and new HTTP semconv for the same request: one group after coalescing. + { + traceId: "t1", + name: "http.server GET", + ms: BASE_MS + 60_000, + service: "api", + durationNs: 5_000_000, + attrs: { "http.method": "GET", "http.route": "/users", "http.status_code": "200" }, + resource: PROD, + }, + { + traceId: "t2", + name: "http.server GET", + ms: BASE_MS + 120_000, + service: "api", + durationNs: 9_000_000, + attrs: { "http.request.method": "GET", "http.route": "/users", "http.response.status_code": "200" }, + resource: PROD, + }, + // A 5xx: HasError without an Error status. + { + traceId: "t3", + name: "POST", + ms: BASE_MS + 180_000, + service: "api", + durationNs: 50_000_000, + attrs: { "http.request.method": "POST", "url.path": "/orders", "http.response.status_code": "503" }, + resource: PROD, + }, + { + traceId: "t4", + name: "cron.tick", + ms: BASE_MS + HOUR_MS + 60_000, + service: "worker", + status: "Error", + durationNs: 1_000_000, + resource: { "deployment.environment": "staging" }, + }, + // Not a root span: never reaches trace_list_mv, so never the rollup. + { + traceId: "t1", + parentSpanId: "root-t1", + name: "db.query", + ms: BASE_MS + 61_000, + service: "api", + durationNs: 2_000_000, + }, +] + +// A second insert block into an hour the first one already wrote: the rollup +// holds two partial rows for that group until a merge, and reads must sum them. +const BATCH_2: ReadonlyArray = [ + { + traceId: "t5", + name: "http.server GET", + ms: BASE_MS + 240_000, + service: "api", + durationNs: 1_000_000, + attrs: { "http.method": "GET", "http.route": "/users", "http.status_code": "200" }, + resource: PROD, + }, +] + +const traceFacetsHourlyBackfill = migrations + .find((migration) => migration.version === 34)! + .statements.find((statement) => typeof statement !== "string")! + +const ROOT_SPAN_COUNT = [...BATCH_1, ...BATCH_2].filter((span) => !span.parentSpanId).length + +const insert = async (spans: ReadonlyArray): Promise => { + const rows = spans + .map( + (span, index) => + `(${quote(ORG_ID)}, ${quote(chDateTime(span.ms))}, ${quote(span.traceId)}, ${quote(`span-${span.traceId}-${index}`)}, ${quote(span.parentSpanId ?? "")}, ${quote(span.name)}, 'Server', ${quote(span.service)}, ${span.durationNs}, ${quote(span.status ?? "Ok")}, '', 1, ${chMap(span.attrs ?? {})}, ${chMap(span.resource ?? {})})`, + ) + .join("\n,") + await clickhouseExec( + `INSERT INTO traces + (OrgId, Timestamp, TraceId, SpanId, ParentSpanId, SpanName, SpanKind, ServiceName, Duration, StatusCode, StatusMessage, SampleRate, SpanAttributes, ResourceAttributes) + VALUES\n${rows}`, + database, + ) +} + +const runJson = async (sql: string): Promise>> => { + const body = await clickhouseExec(`${sql} FORMAT JSON`, database, { + output_format_json_quote_64bit_integers: "0", + }) + return (JSON.parse(body) as { readonly data?: ReadonlyArray> }).data ?? [] +} + +const DIMENSIONS = + "Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError" + +const fromTraceList = () => + runJson( + `SELECT toString(toStartOfHour(Timestamp)) AS Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, + DeploymentEnv, ServiceNamespace, HasError, + count() AS traces, min(Duration) AS durationMin, max(Duration) AS durationMax + FROM trace_list_mv WHERE OrgId = ${quote(ORG_ID)} + GROUP BY ${DIMENSIONS} ORDER BY ${DIMENSIONS}`, + ) + +const fromRollup = () => + runJson( + `SELECT toString(Hour) AS Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, + DeploymentEnv, ServiceNamespace, HasError, + sum(TraceCount) AS traces, min(DurationMin) AS durationMin, max(DurationMax) AS durationMax + FROM trace_facets_hourly WHERE OrgId = ${quote(ORG_ID)} + GROUP BY ${DIMENSIONS} ORDER BY ${DIMENSIONS}`, + ) + +describe.skipIf(!clickhouseE2eEnabled)("trace_facets_hourly materialization", () => { + beforeAll(async () => { + await clickhouseExec(`CREATE DATABASE ${database}`) + await applyRealMigrations(database) + await insert(BATCH_1) + await insert(BATCH_2) + }, 180_000) + + afterAll(async () => { + await clickhouseExec(`DROP DATABASE IF EXISTS ${database}`) + }, 30_000) + + it("rolls up every root span trace_list_mv holds, group for group", async () => { + const expected = await fromTraceList() + const actual = await fromRollup() + + assert.deepStrictEqual(actual, expected) + assert.strictEqual( + actual.reduce((total, row) => total + Number(row.traces), 0), + ROOT_SPAN_COUNT, + ) + // The coalesced semconv spellings and the rewritten span name landed in one group. + assert.deepInclude(actual, { + Hour: chDateTime(BASE_MS).slice(0, 19), + ServiceName: "api", + SpanName: "GET /users", + HttpMethod: "GET", + HttpStatusCode: "200", + DeploymentEnv: "production", + ServiceNamespace: "core", + HasError: 0, + traces: 3, + durationMin: 1_000_000, + durationMax: 9_000_000, + }) + }) + + it("reproduces the same rollup from migration 0034's backfill", async () => { + const expected = await fromRollup() + await clickhouseExec("TRUNCATE TABLE trace_facets_hourly", database) + await clickhouseExec(renderStatementFull(traceFacetsHourlyBackfill, database), database) + + assert.deepStrictEqual(await fromRollup(), expected) + }) +}) diff --git a/packages/backend/src/services/warehouse/warehouse-catalog.ts b/packages/backend/src/services/warehouse/warehouse-catalog.ts index 28a100888b..cd42e48ea4 100644 --- a/packages/backend/src/services/warehouse/warehouse-catalog.ts +++ b/packages/backend/src/services/warehouse/warehouse-catalog.ts @@ -45,6 +45,11 @@ const TABLE_NOTES: Record> = { "Scanners borrow crawler user agents and mostly get 404s: count pages actually read with `HttpStatus < 400` (0 means the span carried no status).", "Sorting key: `(OrgId, Timestamp, TraceId)`; filled forward by its MV from migration 0033, never backfilled.", ], + trace_facets_hourly: [ + "Hourly root-span (one per trace) counts by `ServiceName`, `SpanName`, `HttpMethod`, `HttpStatusCode`, `DeploymentEnv`, `ServiceNamespace` and `HasError`. ALWAYS prefer this over `trace_list_mv` for trace counts or top-N of those dimensions over day-plus windows.", + "Time column is `Hour`; count with `sum(TraceCount)`. `DurationMin`/`DurationMax` are NANOSECONDS (`min`/`max` them); percentiles via `quantilesTDigestMerge(0.5, 0.95)(DurationQuantiles)`.", + "Sorting key: `(OrgId, Hour, ServiceName, SpanName, …)`.", + ], service_overview_spans: [ "Pre-materialized projection of entry-point spans only (Server/Consumer kinds + root spans). Use for per-service request count, error rate, p50/p95/p99 latency.", "`Duration` is NANOSECONDS — divide by 1e6 for ms.", diff --git a/packages/domain/src/clickhouse/backfill.ts b/packages/domain/src/clickhouse/backfill.ts index fd671189fd..7cc3f6250b 100644 --- a/packages/domain/src/clickhouse/backfill.ts +++ b/packages/domain/src/clickhouse/backfill.ts @@ -51,6 +51,7 @@ export const SOURCE_TIME_COLUMNS: Readonly> = { metrics_exponential_histogram: "TimeUnix", service_overview_spans: "Timestamp", service_operations_minutely: "Minute", + trace_list_mv: "Timestamp", session_events: "Timestamp", session_replays: "StartTime", } satisfies Readonly> diff --git a/packages/domain/src/clickhouse/migrations/0034_trace_facets_hourly.ts b/packages/domain/src/clickhouse/migrations/0034_trace_facets_hourly.ts new file mode 100644 index 0000000000..d402e590f7 --- /dev/null +++ b/packages/domain/src/clickhouse/migrations/0034_trace_facets_hourly.ts @@ -0,0 +1,79 @@ +import type { BackfillSpec } from "../backfill" + +const TRACE_FACETS_PROJECTION_SQL = `OrgId, + toStartOfHour(Timestamp) AS Hour, + ServiceName, + SpanName, + HttpMethod, + HttpStatusCode, + DeploymentEnv, + ServiceNamespace, + HasError, + count() AS TraceCount, + min(Duration) AS DurationMin, + max(Duration) AS DurationMax, + quantilesTDigestState(0.5, 0.95)(Duration) AS DurationQuantiles` + +const TRACE_FACETS_GROUP_BY = + "OrgId, Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError" + +/** + * Backfill for the 30 days `trace_list_mv` retains, which is the rollup's own + * retention, so the rollup is complete from the moment the view attaches. The + * backfill and the view share one projection and one source; day-aligned chunks + * are coarser than the hourly groups, so no group straddles a chunk. + */ +export const traceFacetsHourlyBackfill: BackfillSpec = { + kind: "backfill", + target: "trace_facets_hourly", + columns: [ + "OrgId", + "Hour", + "ServiceName", + "SpanName", + "HttpMethod", + "HttpStatusCode", + "DeploymentEnv", + "ServiceNamespace", + "HasError", + "TraceCount", + "DurationMin", + "DurationMax", + "DurationQuantiles", + ], + from: "trace_list_mv", + tsColumn: "Timestamp", + select: TRACE_FACETS_PROJECTION_SQL, + groupBy: TRACE_FACETS_GROUP_BY, +} + +/** + * Migration 0034: `trace_facets_hourly`, the hourly rollup behind the traces + * sidebar facets and duration stats. + * + * Those queries scanned every root span in the window on `trace_list_mv`, and + * past about a day they exceeded the 5s discovery budget. The rollup is + * cascaded off `trace_list_mv`, so its values are the ones the trace list + * filters on. + * + * DROP VIEW → CREATE TABLE → TRUNCATE → backfill → CREATE VIEW, as in 0015: the + * truncate makes a re-apply converge instead of doubling, and it is safe only + * because the view is detached first. `requiredForIngest: false`: nothing + * writes the table directly. + * + * The CREATE statements are the verbatim DDL the schema emitter produced at v34. + * Frozen history: never re-derive it from a later snapshot. + */ +export const migration_0034_trace_facets_hourly = { + version: 34, + description: + "Create trace_facets_hourly + trace_facets_hourly_mv: hourly rollup for the traces sidebar facets", + requiredForIngest: false, + statements: [ + "DROP VIEW IF EXISTS trace_facets_hourly_mv", + "CREATE TABLE IF NOT EXISTS trace_facets_hourly (\n OrgId LowCardinality(String),\n Hour DateTime,\n ServiceName LowCardinality(String),\n SpanName String,\n HttpMethod LowCardinality(String),\n HttpStatusCode LowCardinality(String),\n DeploymentEnv LowCardinality(String),\n ServiceNamespace LowCardinality(String),\n HasError UInt8,\n TraceCount SimpleAggregateFunction(sum, UInt64),\n DurationMin SimpleAggregateFunction(min, UInt64),\n DurationMax SimpleAggregateFunction(max, UInt64),\n DurationQuantiles AggregateFunction(quantilesTDigest(0.5, 0.95), UInt64)\n)\nENGINE = AggregatingMergeTree\nPARTITION BY toYYYYMM(Hour)\nORDER BY (OrgId, Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError)\nTTL toDate(Hour) + INTERVAL 30 DAY", + "TRUNCATE TABLE IF EXISTS trace_facets_hourly", + traceFacetsHourlyBackfill, + `CREATE MATERIALIZED VIEW IF NOT EXISTS trace_facets_hourly_mv TO trace_facets_hourly AS\nSELECT\n ${TRACE_FACETS_PROJECTION_SQL}\n FROM trace_list_mv\n GROUP BY ${TRACE_FACETS_GROUP_BY}`, + ], +} as const diff --git a/packages/domain/src/clickhouse/migrations/index.test.ts b/packages/domain/src/clickhouse/migrations/index.test.ts index 996fdc4672..fc9bb0468b 100644 --- a/packages/domain/src/clickhouse/migrations/index.test.ts +++ b/packages/domain/src/clickhouse/migrations/index.test.ts @@ -38,6 +38,8 @@ import { migration_0030_error_events_attribute_fallback } from "./0030_error_eve import { migration_0031_ai_trace_index_list_columns } from "./0031_ai_trace_index_list_columns" import { migration_0032_ai_trace_index_tool_detail_columns } from "./0032_ai_trace_index_tool_detail_columns" import { migration_0033_ai_crawler_requests } from "./0033_ai_crawler_requests" +import { migration_0034_trace_facets_hourly, traceFacetsHourlyBackfill } from "./0034_trace_facets_hourly" +import { latestSnapshotStatements } from "../../generated/clickhouse-schema" import { clickHouseSchemaVersion, latestMigrationVersion, migrations } from "./index" const backfills = migration_0004_service_namespace_projections.statements.filter( @@ -54,10 +56,10 @@ describe("ClickHouse migrations", () => { it("keeps migrations ordered by version", () => { expect(migrations.map((m) => m.version)).toEqual([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, - 28, 29, 30, 31, 32, 33, + 28, 29, 30, 31, 32, 33, 34, ]) - expect(migrations.at(-1)).toBe(migration_0033_ai_crawler_requests) - expect(latestMigrationVersion).toBe(33) + expect(migrations.at(-1)).toBe(migration_0034_trace_facets_hourly) + expect(latestMigrationVersion).toBe(34) // 0010 and 0014-0020 are read-path only and skipped by the ingest-gating // version; 0021 is not — the gateway writes `session_events`' new identity // columns and `product_events` directly, so a BYO-CH org must apply it @@ -92,6 +94,8 @@ describe("ClickHouse migrations", () => { expect(migration_0032_ai_trace_index_tool_detail_columns.requiredForIngest).toBe(false) // 0033 adds the MV-populated ai_crawler_requests. expect(migration_0033_ai_crawler_requests.requiredForIngest).toBe(false) + // 0034 adds the MV-populated trace_facets_hourly. + expect(migration_0034_trace_facets_hourly.requiredForIngest).toBe(false) }) it("recreates both error-events MVs with the span-attribute exception fallback", () => { @@ -829,6 +833,27 @@ describe("migration 0031 — ai_trace_index list columns", () => { }) }) +describe("migration 0034: trace_facets_hourly", () => { + it("backfills from trace_list_mv with the view detached, then attaches it", () => { + const [drop, table, truncate, backfill, view, ...rest] = migration_0034_trace_facets_hourly.statements + expect(rest).toEqual([]) + expect(drop).toBe("DROP VIEW IF EXISTS trace_facets_hourly_mv") + expect(truncate).toBe("TRUNCATE TABLE IF EXISTS trace_facets_hourly") + expect(backfill).toBe(traceFacetsHourlyBackfill) + expect(traceFacetsHourlyBackfill.from).toBe("trace_list_mv") + expect(traceFacetsHourlyBackfill.target).toBe("trace_facets_hourly") + // The view and the backfill must write the same groups, or backfilled + // hours and live hours disagree with no dedup to catch it. + expect(view).toContain( + `SELECT\n ${traceFacetsHourlyBackfill.select}\n FROM trace_list_mv`, + ) + expect(view).toContain(`GROUP BY ${traceFacetsHourlyBackfill.groupBy}`) + // Frozen DDL: identical to what the emitter produces for the current schema. + expect(latestSnapshotStatements).toContain(table) + expect(latestSnapshotStatements).toContain(view) + }) +}) + describe("migration 0033: ai_crawler_requests", () => { it("creates the table before the view that fills it, with no backfill", () => { const [table, view, ...rest] = migration_0033_ai_crawler_requests.statements diff --git a/packages/domain/src/clickhouse/migrations/index.ts b/packages/domain/src/clickhouse/migrations/index.ts index 0a4ab0562d..7c5a5aecbd 100644 --- a/packages/domain/src/clickhouse/migrations/index.ts +++ b/packages/domain/src/clickhouse/migrations/index.ts @@ -32,6 +32,7 @@ import { migration_0030_error_events_attribute_fallback } from "./0030_error_eve import { migration_0031_ai_trace_index_list_columns } from "./0031_ai_trace_index_list_columns" import { migration_0032_ai_trace_index_tool_detail_columns } from "./0032_ai_trace_index_tool_detail_columns" import { migration_0033_ai_crawler_requests } from "./0033_ai_crawler_requests" +import { migration_0034_trace_facets_hourly } from "./0034_trace_facets_hourly" /** * A migration statement is either a raw SQL string (structural DDL) or a @@ -96,6 +97,7 @@ export const migrations: ReadonlyArray = [ migration_0031_ai_trace_index_list_columns, migration_0032_ai_trace_index_tool_detail_columns, migration_0033_ai_crawler_requests, + migration_0034_trace_facets_hourly, ] as const /** Highest migration `version` bundled — i.e. the schema level a fully-applied diff --git a/packages/domain/src/clickhouse/qualify.ts b/packages/domain/src/clickhouse/qualify.ts index d162d9d914..e4e519142c 100644 --- a/packages/domain/src/clickhouse/qualify.ts +++ b/packages/domain/src/clickhouse/qualify.ts @@ -14,6 +14,7 @@ export const CLICKHOUSE_MV_SOURCE_TABLES: ReadonlyArray = [ "service_map_edges_hourly_ingest", "service_overview_spans", "service_operations_minutely", + "trace_list_mv", "session_events", "session_replays", ] diff --git a/packages/domain/src/generated/clickhouse-schema.ts b/packages/domain/src/generated/clickhouse-schema.ts index ab416e037e..12e147f18e 100644 --- a/packages/domain/src/generated/clickhouse-schema.ts +++ b/packages/domain/src/generated/clickhouse-schema.ts @@ -1,7 +1,7 @@ // This file is generated by scripts/generate-clickhouse-schema.ts // Do not edit manually. -export const projectRevision = "455ff9a387c6e0b10a3083278c020d73ea622f1e30a106fb833bcb2262a94d6f" as const +export const projectRevision = "eee23295aa263ed5dce623418acbe3cdffac84bbea15570e3ac20b4e1d3b9b50" as const export const latestSnapshotStatements: ReadonlyArray = [ "CREATE TABLE IF NOT EXISTS ai_crawler_requests (\n OrgId LowCardinality(String),\n Timestamp DateTime64(9),\n TraceId String,\n ServiceName LowCardinality(String),\n Crawler LowCardinality(String),\n Host LowCardinality(String),\n Path String,\n HttpStatus UInt16\n)\nENGINE = MergeTree\nPARTITION BY toDate(Timestamp)\nORDER BY (OrgId, Timestamp, TraceId)\nTTL toDate(Timestamp) + INTERVAL 30 DAY", @@ -42,6 +42,7 @@ export const latestSnapshotStatements: ReadonlyArray = [ "CREATE TABLE IF NOT EXISTS session_replays (\n OrgId LowCardinality(String),\n SessionId String,\n StartTime DateTime64(9),\n EndTime Nullable(DateTime64(9)),\n DurationMs Nullable(UInt32),\n Status LowCardinality(String),\n UserId String,\n UrlInitial String,\n UserAgent String,\n BrowserName LowCardinality(String),\n OsName LowCardinality(String),\n DeviceType LowCardinality(String),\n Country LowCardinality(String) DEFAULT '',\n ServiceName LowCardinality(String),\n PageViews UInt32 DEFAULT 0,\n ClickCount UInt32 DEFAULT 0,\n ErrorCount UInt32 DEFAULT 0,\n TraceIds Array(String) DEFAULT [],\n ResourceAttributes Map(LowCardinality(String), String),\n Version UInt32,\n VisitorId String DEFAULT '',\n VisitorIsNew UInt8 DEFAULT 0,\n UserEmail String DEFAULT '',\n UserName String DEFAULT '',\n GroupId String DEFAULT '',\n GroupName String DEFAULT '',\n UserTraits Map(String, String) DEFAULT map(),\n Referrer String DEFAULT '',\n ReferrerHost LowCardinality(String) DEFAULT '',\n UtmSource LowCardinality(String) DEFAULT '',\n UtmMedium LowCardinality(String) DEFAULT '',\n UtmCampaign LowCardinality(String) DEFAULT '',\n UtmTerm String DEFAULT '',\n UtmContent String DEFAULT '',\n Host LowCardinality(String) DEFAULT '',\n EntryPath String DEFAULT '',\n ExitPath String DEFAULT '',\n Language LowCardinality(String) DEFAULT '',\n LastActivityAt Nullable(DateTime64(9))\n)\nENGINE = ReplacingMergeTree\nPARTITION BY toDate(StartTime)\nORDER BY (OrgId, SessionId)\nTTL toDate(StartTime) + INTERVAL 30 DAY", "CREATE TABLE IF NOT EXISTS span_metrics_calls_hourly (\n OrgId LowCardinality(String),\n Hour DateTime,\n ServiceName LowCardinality(String),\n MetricName LowCardinality(String),\n SpanKind LowCardinality(String),\n AttrFingerprint UInt64,\n ResourceFingerprint UInt64,\n StartTimeUnix DateTime64(9),\n LastValue AggregateFunction(argMax, Float64, DateTime64(9))\n)\nENGINE = AggregatingMergeTree\nPARTITION BY toDate(Hour)\nORDER BY (OrgId, Hour, ServiceName, MetricName, SpanKind, AttrFingerprint, ResourceFingerprint, StartTimeUnix)\nTTL toDate(Hour) + INTERVAL 90 DAY", "CREATE TABLE IF NOT EXISTS trace_detail_spans (\n OrgId LowCardinality(String),\n Timestamp DateTime64(9),\n TraceId String,\n SpanId String,\n ParentSpanId String,\n SpanName LowCardinality(String),\n SpanKind LowCardinality(String),\n ServiceName LowCardinality(String),\n Duration UInt64 DEFAULT 0,\n StatusCode LowCardinality(String),\n StatusMessage String,\n SpanAttributes Map(LowCardinality(String), String),\n ResourceAttributes Map(LowCardinality(String), String)\n)\nENGINE = MergeTree\nPARTITION BY toDate(Timestamp)\nORDER BY (OrgId, TraceId, SpanId)\nTTL toDate(Timestamp) + INTERVAL 30 DAY", + "CREATE TABLE IF NOT EXISTS trace_facets_hourly (\n OrgId LowCardinality(String),\n Hour DateTime,\n ServiceName LowCardinality(String),\n SpanName String,\n HttpMethod LowCardinality(String),\n HttpStatusCode LowCardinality(String),\n DeploymentEnv LowCardinality(String),\n ServiceNamespace LowCardinality(String),\n HasError UInt8,\n TraceCount SimpleAggregateFunction(sum, UInt64),\n DurationMin SimpleAggregateFunction(min, UInt64),\n DurationMax SimpleAggregateFunction(max, UInt64),\n DurationQuantiles AggregateFunction(quantilesTDigest(0.5, 0.95), UInt64)\n)\nENGINE = AggregatingMergeTree\nPARTITION BY toYYYYMM(Hour)\nORDER BY (OrgId, Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError)\nTTL toDate(Hour) + INTERVAL 30 DAY", "CREATE TABLE IF NOT EXISTS trace_list_mv (\n OrgId LowCardinality(String),\n TraceId String,\n Timestamp DateTime,\n ServiceName LowCardinality(String),\n SpanName String,\n SpanKind LowCardinality(String),\n Duration UInt64,\n StatusCode LowCardinality(String),\n HttpMethod LowCardinality(String),\n HttpRoute String,\n HttpStatusCode LowCardinality(String),\n DeploymentEnv LowCardinality(String),\n HasError UInt8,\n TraceState String,\n ServiceNamespace LowCardinality(String),\n INDEX idx_service_namespace ServiceNamespace TYPE set(1000) GRANULARITY 4\n)\nENGINE = MergeTree\nPARTITION BY toDate(Timestamp)\nORDER BY (OrgId, Timestamp, TraceId)\nTTL Timestamp + INTERVAL 30 DAY", "CREATE TABLE IF NOT EXISTS traces (\n OrgId LowCardinality(String),\n Timestamp DateTime64(9),\n TraceId String,\n SpanId String,\n ParentSpanId String,\n TraceState String,\n SpanName LowCardinality(String),\n SpanKind LowCardinality(String),\n ServiceName LowCardinality(String),\n ResourceSchemaUrl String,\n ResourceAttributes Map(LowCardinality(String), String),\n ScopeSchemaUrl String,\n ScopeName String,\n ScopeVersion String,\n ScopeAttributes Map(LowCardinality(String), String),\n Duration UInt64 DEFAULT 0,\n StatusCode LowCardinality(String),\n StatusMessage String,\n SpanAttributes Map(LowCardinality(String), String),\n EventsTimestamp Array(DateTime64(9)),\n EventsName Array(LowCardinality(String)),\n EventsAttributes Array(Map(LowCardinality(String), String)),\n LinksTraceId Array(String),\n LinksSpanId Array(String),\n LinksTraceState Array(String),\n LinksAttributes Array(Map(LowCardinality(String), String)),\n SampleRate Float64 DEFAULT multiIf(SpanAttributes['SampleRate'] != '' AND toFloat64OrZero(SpanAttributes['SampleRate']) >= 1.0, toFloat64OrZero(SpanAttributes['SampleRate']), match(TraceState, 'th:[0-9a-f]+'), 1.0 / greatest(1.0 - reinterpretAsUInt64(reverse(unhex(rightPad(extract(TraceState, 'th:([0-9a-f]+)'), 16, '0')))) / pow(2.0, 64), 0.0001), 1.0),\n IsEntryPoint UInt8 DEFAULT if(SpanKind IN ('Server', 'Consumer') OR ParentSpanId = '', 1, 0),\n ResourceAttributeItems Array(String) DEFAULT arrayMap((k, v) -> concat(k, char(31), v), mapKeys(ResourceAttributes), mapValues(ResourceAttributes)),\n ScopeAttributeItems Array(String) DEFAULT arrayMap((k, v) -> concat(k, char(31), v), mapKeys(ScopeAttributes), mapValues(ScopeAttributes)),\n SpanAttributeItems Array(String) DEFAULT arrayMap((k, v) -> concat(k, char(31), v), mapKeys(SpanAttributes), mapValues(SpanAttributes)),\n INDEX idx_trace_id TraceId TYPE bloom_filter(0.01) GRANULARITY 1,\n INDEX idx_span_attr_keys mapKeys(SpanAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,\n INDEX idx_span_attr_vals mapValues(SpanAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,\n INDEX idx_resource_attr_keys mapKeys(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,\n INDEX idx_resource_attr_vals mapValues(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,\n INDEX idx_scope_attr_keys mapKeys(ScopeAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,\n INDEX idx_scope_attr_vals mapValues(ScopeAttributes) TYPE bloom_filter(0.01) GRANULARITY 1\n)\nENGINE = MergeTree\nPARTITION BY toDate(Timestamp)\nORDER BY (OrgId, ServiceName, SpanName, toDateTime(Timestamp))\nTTL toDate(Timestamp) + INTERVAL 30 DAY", "CREATE TABLE IF NOT EXISTS traces_aggregates_hourly (\n OrgId LowCardinality(String),\n Hour DateTime,\n ServiceName LowCardinality(String),\n SpanName LowCardinality(String),\n SpanKind LowCardinality(String),\n StatusCode LowCardinality(String),\n IsEntryPoint UInt8,\n DeploymentEnv LowCardinality(String),\n WeightedCount SimpleAggregateFunction(sum, Float64),\n WeightedDurationSum SimpleAggregateFunction(sum, Float64),\n WeightedErrorCount SimpleAggregateFunction(sum, Float64),\n DurationQuantiles AggregateFunction(quantilesTDigestWeighted(0.5, 0.95, 0.99), UInt64, UInt32),\n DurationMin SimpleAggregateFunction(min, UInt64),\n DurationMax SimpleAggregateFunction(max, UInt64)\n)\nENGINE = AggregatingMergeTree\nPARTITION BY toDate(Hour)\nORDER BY (OrgId, Hour, ServiceName, SpanName, SpanKind, StatusCode, IsEntryPoint, DeploymentEnv)\nTTL toDate(Hour) + INTERVAL 365 DAY", @@ -82,6 +83,7 @@ export const latestSnapshotStatements: ReadonlyArray = [ "CREATE MATERIALIZED VIEW IF NOT EXISTS service_usage_traces_mv TO service_usage AS\nSELECT\n OrgId,\n ServiceName,\n toStartOfHour(toDateTime(Timestamp)) AS Hour,\n 0 AS LogCount,\n 0 AS LogSizeBytes,\n count() AS TraceCount,\n sum(length(SpanName) + 300) AS TraceSizeBytes,\n 0 AS SumMetricCount,\n 0 AS SumMetricSizeBytes,\n 0 AS GaugeMetricCount,\n 0 AS GaugeMetricSizeBytes,\n 0 AS HistogramMetricCount,\n 0 AS HistogramMetricSizeBytes,\n 0 AS ExpHistogramMetricCount,\n 0 AS ExpHistogramMetricSizeBytes\n FROM traces\n GROUP BY OrgId, ServiceName, Hour", "CREATE MATERIALIZED VIEW IF NOT EXISTS span_metrics_calls_hourly_mv TO span_metrics_calls_hourly AS\nSELECT\n OrgId,\n toStartOfHour(toDateTime(TimeUnix)) AS Hour,\n ServiceName,\n MetricName,\n Attributes['span.kind'] AS SpanKind,\n cityHash64(mapKeys(Attributes), mapValues(Attributes)) AS AttrFingerprint,\n cityHash64(mapKeys(ResourceAttributes), mapValues(ResourceAttributes)) AS ResourceFingerprint,\n StartTimeUnix,\n argMaxState(Value, TimeUnix) AS LastValue\n FROM metrics_sum\n -- 'traces.span.metrics.calls' is the name the collector actually emits:\n -- spanmetricsconnector output is namespaced by the pipeline it is attached\n -- to. Without it this MV matched nothing and the target sat at 0 rows since\n -- it was created, while ~880k rows / 2 days of the real counter flowed past\n -- into metrics_sum and every read fell back to the raw window-function scan\n -- (~7s p95 -- see queries/metrics.ts). Keep this list in sync with\n -- SPAN_METRICS_CALLS_NAMES on the read side.\n WHERE MetricName IN ('span.metrics.calls', 'calls', 'traces.span.metrics.calls') AND IsMonotonic\n GROUP BY OrgId, Hour, ServiceName, MetricName, SpanKind, AttrFingerprint, ResourceFingerprint, StartTimeUnix", "CREATE MATERIALIZED VIEW IF NOT EXISTS trace_detail_spans_mv TO trace_detail_spans AS\nSELECT\n OrgId,\n Timestamp,\n TraceId,\n SpanId,\n ParentSpanId,\n SpanName,\n SpanKind,\n ServiceName,\n Duration,\n StatusCode,\n StatusMessage,\n SpanAttributes,\n ResourceAttributes\n FROM traces", + "CREATE MATERIALIZED VIEW IF NOT EXISTS trace_facets_hourly_mv TO trace_facets_hourly AS\nSELECT\n OrgId,\n toStartOfHour(Timestamp) AS Hour,\n ServiceName,\n SpanName,\n HttpMethod,\n HttpStatusCode,\n DeploymentEnv,\n ServiceNamespace,\n HasError,\n count() AS TraceCount,\n min(Duration) AS DurationMin,\n max(Duration) AS DurationMax,\n quantilesTDigestState(0.5, 0.95)(Duration) AS DurationQuantiles\n FROM trace_list_mv\n GROUP BY OrgId, Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError", "CREATE MATERIALIZED VIEW IF NOT EXISTS trace_list_mv_mv TO trace_list_mv AS\nSELECT\n OrgId,\n TraceId,\n toDateTime(Timestamp) AS Timestamp,\n ServiceName,\n if(\n (SpanName LIKE 'http.server %' OR SpanName IN ('GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS'))\n AND (SpanAttributes['http.route'] != '' OR SpanAttributes['url.path'] != ''),\n concat(\n if(SpanName LIKE 'http.server %', replaceOne(SpanName, 'http.server ', ''), SpanName),\n ' ',\n if(SpanAttributes['http.route'] != '', SpanAttributes['http.route'], SpanAttributes['url.path'])\n ),\n SpanName\n ) AS SpanName,\n SpanKind,\n Duration,\n StatusCode,\n if(SpanAttributes['http.method'] != '', SpanAttributes['http.method'], SpanAttributes['http.request.method']) AS HttpMethod,\n if(SpanAttributes['http.route'] != '', SpanAttributes['http.route'], if(SpanAttributes['url.path'] != '', SpanAttributes['url.path'], SpanAttributes['http.target'])) AS HttpRoute,\n if(SpanAttributes['http.status_code'] != '', SpanAttributes['http.status_code'], SpanAttributes['http.response.status_code']) AS HttpStatusCode,\n coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv,\n toUInt8(\n StatusCode = 'Error'\n OR (SpanAttributes['http.status_code'] != '' AND toUInt16OrZero(SpanAttributes['http.status_code']) >= 500)\n OR (SpanAttributes['http.response.status_code'] != '' AND toUInt16OrZero(SpanAttributes['http.response.status_code']) >= 500)\n ) AS HasError,\n TraceState,\n ResourceAttributes['service.namespace'] AS ServiceNamespace\n FROM traces\n WHERE ParentSpanId = ''", "CREATE MATERIALIZED VIEW IF NOT EXISTS trace_resource_attribute_keys_mv TO attribute_keys_hourly AS\nSELECT\n OrgId,\n toStartOfHour(toDateTime(Timestamp)) AS Hour,\n arrayJoin(mapKeys(ResourceAttributes)) AS AttributeKey,\n 'resource' AS AttributeScope,\n count() AS UsageCount\n FROM traces\n WHERE ResourceAttributes != map()\n GROUP BY OrgId, Hour, AttributeKey, AttributeScope", "CREATE MATERIALIZED VIEW IF NOT EXISTS trace_resource_attribute_values_mv TO attribute_values_hourly AS\nSELECT\n OrgId,\n toStartOfHour(toDateTime(Timestamp)) AS Hour,\n AttributeKey,\n AttributeValue,\n 'resource' AS AttributeScope,\n count() AS UsageCount\n FROM traces\n ARRAY JOIN\n mapKeys(ResourceAttributes) AS AttributeKey,\n mapValues(ResourceAttributes) AS AttributeValue\n WHERE AttributeValue != ''\n AND length(AttributeValue) <= 128\n AND NOT (length(AttributeValue) > 4 AND match(AttributeValue, '^[0-9]+([.][0-9]+)?$'))\n AND NOT match(AttributeKey, '(_id|[.]id|Id|_ns)$')\n AND AttributeKey NOT LIKE 'http.request.header.%'\n AND AttributeKey NOT LIKE 'http.response.header.%'\n GROUP BY OrgId, Hour, AttributeKey, AttributeValue, AttributeScope", diff --git a/packages/domain/src/generated/tinybird-project-manifest.ts b/packages/domain/src/generated/tinybird-project-manifest.ts index 15a9162774..287d020512 100644 --- a/packages/domain/src/generated/tinybird-project-manifest.ts +++ b/packages/domain/src/generated/tinybird-project-manifest.ts @@ -1,7 +1,7 @@ // This file is generated by scripts/generate-tinybird-project-manifest.ts // Do not edit manually. -export const projectRevision = "455ff9a387c6e0b10a3083278c020d73ea622f1e30a106fb833bcb2262a94d6f" as const +export const projectRevision = "eee23295aa263ed5dce623418acbe3cdffac84bbea15570e3ac20b4e1d3b9b50" as const export const datasources = [ { @@ -194,6 +194,11 @@ export const datasources = [ content: 'DESCRIPTION >\n All spans for a trace, sorted by TraceId for fast detail lookups. Populated by materialized view.\n\nSCHEMA >\n OrgId LowCardinality(String),\n Timestamp DateTime64(9),\n TraceId String,\n SpanId String,\n ParentSpanId String,\n SpanName LowCardinality(String),\n SpanKind LowCardinality(String),\n ServiceName LowCardinality(String),\n Duration UInt64 DEFAULT 0,\n StatusCode LowCardinality(String),\n StatusMessage String,\n SpanAttributes Map(LowCardinality(String), String),\n ResourceAttributes Map(LowCardinality(String), String)\n\nENGINE "MergeTree"\nENGINE_PARTITION_KEY "toDate(Timestamp)"\nENGINE_SORTING_KEY "OrgId, TraceId, SpanId"\nENGINE_TTL "toDate(Timestamp) + INTERVAL 30 DAY"', }, + { + name: "trace_facets_hourly", + content: + 'DESCRIPTION >\n Hourly root-span counts and duration state per service, span name, HTTP method/status, environment, namespace and error flag. Traces sidebar facets. Populated by materialized view from trace_list_mv.\n\nSCHEMA >\n OrgId LowCardinality(String),\n Hour DateTime,\n ServiceName LowCardinality(String),\n SpanName String,\n HttpMethod LowCardinality(String),\n HttpStatusCode LowCardinality(String),\n DeploymentEnv LowCardinality(String),\n ServiceNamespace LowCardinality(String),\n HasError UInt8,\n TraceCount SimpleAggregateFunction(sum, UInt64),\n DurationMin SimpleAggregateFunction(min, UInt64),\n DurationMax SimpleAggregateFunction(max, UInt64),\n DurationQuantiles AggregateFunction(quantilesTDigest(0.5, 0.95), UInt64)\n\nENGINE "AggregatingMergeTree"\nENGINE_PARTITION_KEY "toYYYYMM(Hour)"\nENGINE_SORTING_KEY "OrgId, Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError"\nENGINE_TTL "toDate(Hour) + INTERVAL 30 DAY"', + }, { name: "trace_list_mv", content: @@ -397,6 +402,11 @@ export const pipes = [ content: "DESCRIPTION >\n Populates trace_detail_spans with all spans re-sorted by TraceId for fast detail lookups\n\nNODE trace_detail_spans_mv_node\nSQL >\n SELECT\n OrgId,\n Timestamp,\n TraceId,\n SpanId,\n ParentSpanId,\n SpanName,\n SpanKind,\n ServiceName,\n Duration,\n StatusCode,\n StatusMessage,\n SpanAttributes,\n ResourceAttributes\n FROM traces\n\nTYPE MATERIALIZED\nDATASOURCE trace_detail_spans", }, + { + name: "trace_facets_hourly_mv", + content: + "DESCRIPTION >\n Rolls trace_list_mv up hourly by the traces sidebar facet dimensions, with root-span counts and duration state.\n\nNODE trace_facets_hourly_mv_node\nSQL >\n SELECT\n OrgId,\n toStartOfHour(Timestamp) AS Hour,\n ServiceName,\n SpanName,\n HttpMethod,\n HttpStatusCode,\n DeploymentEnv,\n ServiceNamespace,\n HasError,\n count() AS TraceCount,\n min(Duration) AS DurationMin,\n max(Duration) AS DurationMax,\n quantilesTDigestState(0.5, 0.95)(Duration) AS DurationQuantiles\n FROM trace_list_mv\n GROUP BY OrgId, Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError\n\nTYPE MATERIALIZED\nDATASOURCE trace_facets_hourly", + }, { name: "trace_list_mv_mv", content: diff --git a/packages/domain/src/tinybird/datasources.ts b/packages/domain/src/tinybird/datasources.ts index fa140c29b3..04dc54e029 100644 --- a/packages/domain/src/tinybird/datasources.ts +++ b/packages/domain/src/tinybird/datasources.ts @@ -1045,6 +1045,59 @@ export const traceListMv = defineDatasource("trace_list_mv", { export type TraceListMvRow = InferRow +/** + * Hourly rollup of `trace_list_mv` over the traces sidebar's facet dimensions, + * for `tracesFacetsQuery` and `tracesDurationStatsQuery`. + * + * Those queries count root spans per facet value over the page's window. On + * `trace_list_mv` that is a scan of every root span in the window (~14M/day + * for a busy org), and past about a day it exceeds the 5s discovery budget. + * The dimensions collapse to a few hundred combinations per org-hour, so the + * same answer is a read of a few thousand rows here. + * + * Every dimension is in the sorting key: on an AggregatingMergeTree a non-key + * column merges rows together and sums across the values you meant to keep + * apart. Cascaded off `trace_list_mv`, so the span-name rewrite and the HTTP + * semconv coalescing are the ones the trace list already filters on. + */ +export const traceFacetsHourly = defineDatasource("trace_facets_hourly", { + description: + "Hourly root-span counts and duration state per service, span name, HTTP method/status, environment, namespace and error flag. Traces sidebar facets. Populated by materialized view from trace_list_mv.", + jsonPaths: false, + schema: { + OrgId: t.string().lowCardinality(), + Hour: t.dateTime(), + ServiceName: t.string().lowCardinality(), + SpanName: t.string(), + HttpMethod: t.string().lowCardinality(), + HttpStatusCode: t.string().lowCardinality(), + DeploymentEnv: t.string().lowCardinality(), + ServiceNamespace: t.string().lowCardinality(), + HasError: t.uint8(), + TraceCount: t.simpleAggregateFunction("sum", t.uint64()), + DurationMin: t.simpleAggregateFunction("min", t.uint64()), + DurationMax: t.simpleAggregateFunction("max", t.uint64()), + DurationQuantiles: t.aggregateFunction("quantilesTDigest(0.5, 0.95)", t.uint64()), + }, + engine: engine.aggregatingMergeTree({ + partitionKey: "toYYYYMM(Hour)", + sortingKey: [ + "OrgId", + "Hour", + "ServiceName", + "SpanName", + "HttpMethod", + "HttpStatusCode", + "DeploymentEnv", + "ServiceNamespace", + "HasError", + ], + ttl: "toDate(Hour) + INTERVAL 30 DAY", + }), +}) + +export type TraceFacetsHourlyRow = InferRow + /** * All spans for a given trace, re-sorted by TraceId for fast detail lookups. * Populated by materialized view, not direct ingestion. diff --git a/packages/domain/src/tinybird/materializations.ts b/packages/domain/src/tinybird/materializations.ts index 6b90ccdb57..d4a95a8f0c 100644 --- a/packages/domain/src/tinybird/materializations.ts +++ b/packages/domain/src/tinybird/materializations.ts @@ -29,6 +29,7 @@ import { aiCrawlerRequests, traceDetailSpans, traceListMv, + traceFacetsHourly, attributeKeysHourly, attributeValuesHourly, tracesAggregatesHourly, @@ -1172,6 +1173,41 @@ export const traceListMvMv = defineMaterializedView("trace_list_mv_mv", { ], }) +/** + * Cascading hourly rollup of `trace_list_mv` for the traces sidebar facets. It + * consumes the root-span insert stream `trace_list_mv_mv` already produced, so + * its dimensions are the trace list's exact values rather than a fourth copy of + * the span-name rewrite. + */ +export const traceFacetsHourlyMv = defineMaterializedView("trace_facets_hourly_mv", { + description: + "Rolls trace_list_mv up hourly by the traces sidebar facet dimensions, with root-span counts and duration state.", + datasource: traceFacetsHourly, + nodes: [ + node({ + name: "trace_facets_hourly_mv_node", + sql: ` + SELECT + OrgId, + toStartOfHour(Timestamp) AS Hour, + ServiceName, + SpanName, + HttpMethod, + HttpStatusCode, + DeploymentEnv, + ServiceNamespace, + HasError, + count() AS TraceCount, + min(Duration) AS DurationMin, + max(Duration) AS DurationMax, + quantilesTDigestState(0.5, 0.95)(Duration) AS DurationQuantiles + FROM trace_list_mv + GROUP BY OrgId, Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError + `, + }), + ], +}) + // Attribute key aggregation MVs export const traceSpanAttributeKeysMv = defineMaterializedView("trace_span_attribute_keys_mv", { diff --git a/packages/domain/src/tinybird/materialized-projection-order.test.ts b/packages/domain/src/tinybird/materialized-projection-order.test.ts index cd47ad7a80..2609561c97 100644 --- a/packages/domain/src/tinybird/materialized-projection-order.test.ts +++ b/packages/domain/src/tinybird/materialized-projection-order.test.ts @@ -116,6 +116,7 @@ describe("materialized projection order", () => { ["service_overview_hourly", "service_overview_hourly_mv"], ["service_operations_hourly", "service_operations_hourly_mv"], ["trace_list_mv", "trace_list_mv_mv"], + ["trace_facets_hourly", "trace_facets_hourly_mv"], ["logs_aggregates_hourly", "logs_aggregates_hourly_mv"], ["error_events_by_time", "error_events_by_time_mv"], ["error_fingerprints_minutely", "error_fingerprints_minutely_mv"], diff --git a/packages/domain/src/tinybird/retention-matrix.test.ts b/packages/domain/src/tinybird/retention-matrix.test.ts index bdd48d0fc6..c48ac81962 100644 --- a/packages/domain/src/tinybird/retention-matrix.test.ts +++ b/packages/domain/src/tinybird/retention-matrix.test.ts @@ -45,6 +45,9 @@ const RETENTION_DAYS = { session_replays: 30, span_metrics_calls_hourly: 90, trace_detail_spans: 30, + // Matches its source, trace_list_mv: facet counts past the list's own window + // would offer values that filter the list down to nothing. + trace_facets_hourly: 30, trace_list_mv: 30, traces: 30, traces_aggregates_hourly: 365, From 237718faec2a0decabd040b5a3fc8841c49f2088 Mon Sep 17 00:00:00 2001 From: JeremyFunk Date: Mon, 28 Sep 2026 09:54:10 +0200 Subject: [PATCH 2/7] perf(traces): read sidebar facets from the trace_facets_hourly rollup tracesFacets and tracesDurationStats now splice two tiers through rollup-splice: trace_facets_hourly answers every whole hour of the window and trace_list_mv only the partial hour at each end. The raw scan is bounded to under two hours whatever the window, instead of every root span in it (~100M rows for a 7-day window on a busy org, which timed out at 5s). Facet counts are summed across the tiers per value before the top-N cut. Duration stats merge the t-digest state across tiers and take min/max only from tiers that saw a row, so an empty tier's 0 never wins. Duration bounds and attribute/resource filters need the individual root span, so those requests keep reading trace_list_mv for the whole window (canUseTraceFacetsRollup). The splice-gate regex now counts trace_list_mv as a raw table, fixtures cover both routes, and a ClickHouse e2e checks the splice returns the same facets and duration extremes as the trace_list_mv-only route on a window that starts and ends mid-hour. --- ...rly-materialization.clickhouse.e2e.test.ts | 44 +- .../src/__sql_baseline__/catalog.sql | 1298 +++++++++++++++-- .../query-engine/src/benchmark/catalog.ts | 25 +- packages/query-engine/src/ch/ch.test.ts | 4 +- packages/query-engine/src/ch/index.ts | 1 + .../src/ch/queries/errors.test.ts | 63 +- .../query-engine/src/ch/queries/errors.ts | 333 +++-- packages/query-engine/src/ch/tables.ts | 18 + 8 files changed, 1571 insertions(+), 215 deletions(-) diff --git a/packages/backend/src/services/warehouse/trace-facets-hourly-materialization.clickhouse.e2e.test.ts b/packages/backend/src/services/warehouse/trace-facets-hourly-materialization.clickhouse.e2e.test.ts index 604e6e4a7a..75e3f7adac 100644 --- a/packages/backend/src/services/warehouse/trace-facets-hourly-materialization.clickhouse.e2e.test.ts +++ b/packages/backend/src/services/warehouse/trace-facets-hourly-materialization.clickhouse.e2e.test.ts @@ -9,6 +9,8 @@ import { afterAll, assert, beforeAll, describe, it } from "@effect/vitest" import { migrations, renderStatementFull } from "@maple/domain/clickhouse" +import * as CH from "@maple/query-engine/ch" +import { normalizeSqlForClickHouseClient } from "@maple/query-engine/execution" import { applyRealMigrations, clickhouseE2eEnabled, @@ -97,6 +99,15 @@ const BATCH_1: ReadonlyArray = [ // A second insert block into an hour the first one already wrote: the rollup // holds two partial rows for that group until a merge, and reads must sum them. const BATCH_2: ReadonlyArray = [ + { + traceId: "t6", + name: "http.server GET", + ms: BASE_MS + HOUR_MS + 120_000, + service: "api", + durationNs: 7_000_000, + attrs: { "http.method": "GET", "http.route": "/users", "http.status_code": "200" }, + resource: PROD, + }, { traceId: "t5", name: "http.server GET", @@ -130,7 +141,7 @@ const insert = async (spans: ReadonlyArray): Promise => { } const runJson = async (sql: string): Promise>> => { - const body = await clickhouseExec(`${sql} FORMAT JSON`, database, { + const body = await clickhouseExec(sql.includes("FORMAT JSON") ? sql : `${sql} FORMAT JSON`, database, { output_format_json_quote_64bit_integers: "0", }) return (JSON.parse(body) as { readonly data?: ReadonlyArray> }).data ?? [] @@ -194,6 +205,37 @@ describe.skipIf(!clickhouseE2eEnabled)("trace_facets_hourly materialization", () }) }) + // Starts mid-hour, after t1: the first hour is a raw edge that must drop t1, + // the second a whole hour the rollup answers, and the end a partial hour. + it("answers the sidebar facets and duration stats identically from the splice and from trace_list_mv", async () => { + const window = { + orgId: ORG_ID, + startTime: chDateTime(BASE_MS + 90_000).slice(0, 19), + endTime: chDateTime(BASE_MS + 2 * HOUR_MS + 600_000).slice(0, 19), + } + const run = async (sql: string) => runJson(normalizeSqlForClickHouseClient(sql)) + const byFacet = (rows: ReadonlyArray>) => + [...rows].sort((a, b) => `${a.facetType}:${a.name}`.localeCompare(`${b.facetType}:${b.name}`)) + + // `minDurationMs: 0` filters nothing and forces the trace_list_mv-only route. + const spliced = CH.compileUnionUnsafe(CH.tracesFacetsQuery({}), window).sql + const raw = CH.compileUnionUnsafe(CH.tracesFacetsQuery({ minDurationMs: 0 }), window).sql + assert.include(spliced, "trace_facets_hourly") + assert.notInclude(raw, "trace_facets_hourly") + + const splicedRows = byFacet(await run(spliced)) + assert.deepStrictEqual(splicedRows, byFacet(await run(raw))) + assert.deepInclude(splicedRows, { name: "GET /users", count: 3, facetType: "spanName" }) + + const [splicedStats] = await run(CH.compileUnsafe(CH.tracesDurationStatsQuery({}), window).sql) + const [rawStats] = await run( + CH.compileUnsafe(CH.tracesDurationStatsQuery({ minDurationMs: 0 }), window).sql, + ) + assert.strictEqual(splicedStats!.minDurationMs, rawStats!.minDurationMs) + assert.strictEqual(splicedStats!.maxDurationMs, rawStats!.maxDurationMs) + assert.isAbove(Number(splicedStats!.p50DurationMs), 0) + }) + it("reproduces the same rollup from migration 0034's backfill", async () => { const expected = await fromRollup() await clickhouseExec("TRUNCATE TABLE trace_facets_hourly", database) diff --git a/packages/query-engine/src/__sql_baseline__/catalog.sql b/packages/query-engine/src/__sql_baseline__/catalog.sql index 906f5d4224..600fc1d264 100644 --- a/packages/query-engine/src/__sql_baseline__/catalog.sql +++ b/packages/query-engine/src/__sql_baseline__/catalog.sql @@ -10349,7 +10349,37 @@ SELECT LIMIT 20 FORMAT JSON --- pipe:traces_duration_stats:default:baseline [dc834b4b] +-- pipe:traces_duration_stats:default:baseline [05b1bc71] +SELECT + minIf(durationMin, traceCount > 0) / 1000000 AS minDurationMs, + maxIf(durationMax, traceCount > 0) / 1000000 AS maxDurationMs, + ifNull(ifNotFinite(arrayElement(quantilesTDigestMerge(0.5, 0.95)(durationQuantiles), 1) / 1000000, 0), 0) AS p50DurationMs, + ifNull(ifNotFinite(arrayElement(quantilesTDigestMerge(0.5, 0.95)(durationQuantiles), 2) / 1000000, 0), 0) AS p95DurationMs + FROM ( +SELECT + count() AS traceCount, + min(Duration) AS durationMin, + max(Duration) AS durationMax, + quantilesTDigestState(0.5, 0.95)(Duration) AS durationQuantiles + FROM trace_list_mv + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) +UNION ALL +SELECT + sum(TraceCount) AS traceCount, + min(DurationMin) AS durationMin, + max(DurationMax) AS durationMax, + quantilesTDigestMergeState(0.5, 0.95)(DurationQuantiles) AS durationQuantiles + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) +) AS duration_tiers + FORMAT JSON + +-- pipe:traces_duration_stats:duration-bounded:baseline [711880b8] SELECT min(Duration) / 1000000 AS minDurationMs, max(Duration) / 1000000 AS maxDurationMs, @@ -10359,6 +10389,7 @@ SELECT WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' + AND Duration >= 100000000 FORMAT JSON -- pipe:traces_facets:attribute-filtered:baseline [952a9803] @@ -10964,205 +10995,862 @@ SELECT AND HasError = 1 FORMAT JSON --- pipe:traces_facets:default:baseline [3a9bffe8] +-- pipe:traces_facets:default:baseline [2008761a] SELECT - ServiceName AS name, - count() AS count, + name AS name, + sum(count) AS count, 'service' AS facetType + FROM ( +SELECT + ServiceName AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + GROUP BY name +UNION ALL +SELECT + ServiceName AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + GROUP BY name +) AS service_tiers GROUP BY name ORDER BY count DESC LIMIT 50 UNION ALL SELECT - SpanName AS name, - count() AS count, + name AS name, + sum(count) AS count, 'spanName' AS facetType + FROM ( +SELECT + SpanName AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND SpanName != '' + GROUP BY name +UNION ALL +SELECT + SpanName AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) AND SpanName != '' GROUP BY name +) AS spanName_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - HttpMethod AS name, - count() AS count, + name AS name, + sum(count) AS count, 'httpMethod' AS facetType + FROM ( +SELECT + HttpMethod AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND HttpMethod != '' + GROUP BY name +UNION ALL +SELECT + HttpMethod AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) AND HttpMethod != '' GROUP BY name +) AS httpMethod_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - HttpStatusCode AS name, - count() AS count, + name AS name, + sum(count) AS count, 'httpStatus' AS facetType + FROM ( +SELECT + HttpStatusCode AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND HttpStatusCode != '' + GROUP BY name +UNION ALL +SELECT + HttpStatusCode AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) AND HttpStatusCode != '' GROUP BY name +) AS httpStatus_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - DeploymentEnv AS name, - count() AS count, + name AS name, + sum(count) AS count, 'deploymentEnv' AS facetType + FROM ( +SELECT + DeploymentEnv AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND DeploymentEnv != '' + GROUP BY name +UNION ALL +SELECT + DeploymentEnv AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) AND DeploymentEnv != '' GROUP BY name +) AS deploymentEnv_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - ServiceNamespace AS name, - count() AS count, + name AS name, + sum(count) AS count, 'serviceNamespace' AS facetType + FROM ( +SELECT + ServiceNamespace AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND ServiceNamespace != '' + GROUP BY name +UNION ALL +SELECT + ServiceNamespace AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) AND ServiceNamespace != '' GROUP BY name +) AS serviceNamespace_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT 'error' AS name, - count() AS count, + sum(count) AS count, 'errorCount' AS facetType + FROM ( +SELECT + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND HasError = 1 +UNION ALL +SELECT + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) AND HasError = 1 +) AS errorCount_tiers FORMAT JSON --- pipe:traces_facets:default:bloom [3a9bffe8] +-- pipe:traces_facets:default:bloom [2008761a] SELECT - ServiceName AS name, - count() AS count, + name AS name, + sum(count) AS count, 'service' AS facetType + FROM ( +SELECT + ServiceName AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + GROUP BY name +UNION ALL +SELECT + ServiceName AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + GROUP BY name +) AS service_tiers GROUP BY name ORDER BY count DESC LIMIT 50 UNION ALL SELECT - SpanName AS name, - count() AS count, + name AS name, + sum(count) AS count, 'spanName' AS facetType + FROM ( +SELECT + SpanName AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND SpanName != '' + GROUP BY name +UNION ALL +SELECT + SpanName AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) AND SpanName != '' GROUP BY name +) AS spanName_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - HttpMethod AS name, - count() AS count, + name AS name, + sum(count) AS count, 'httpMethod' AS facetType + FROM ( +SELECT + HttpMethod AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND HttpMethod != '' + GROUP BY name +UNION ALL +SELECT + HttpMethod AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) AND HttpMethod != '' GROUP BY name +) AS httpMethod_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - HttpStatusCode AS name, - count() AS count, + name AS name, + sum(count) AS count, 'httpStatus' AS facetType + FROM ( +SELECT + HttpStatusCode AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND HttpStatusCode != '' + GROUP BY name +UNION ALL +SELECT + HttpStatusCode AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) AND HttpStatusCode != '' GROUP BY name +) AS httpStatus_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - DeploymentEnv AS name, - count() AS count, + name AS name, + sum(count) AS count, 'deploymentEnv' AS facetType + FROM ( +SELECT + DeploymentEnv AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND DeploymentEnv != '' + GROUP BY name +UNION ALL +SELECT + DeploymentEnv AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) AND DeploymentEnv != '' GROUP BY name +) AS deploymentEnv_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - ServiceNamespace AS name, - count() AS count, + name AS name, + sum(count) AS count, 'serviceNamespace' AS facetType + FROM ( +SELECT + ServiceNamespace AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND ServiceNamespace != '' + GROUP BY name +UNION ALL +SELECT + ServiceNamespace AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) AND ServiceNamespace != '' GROUP BY name +) AS serviceNamespace_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT 'error' AS name, - count() AS count, + sum(count) AS count, 'errorCount' AS facetType + FROM ( +SELECT + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND HasError = 1 +UNION ALL +SELECT + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) AND HasError = 1 +) AS errorCount_tiers FORMAT JSON --- pipe:traces_facets:default:text [3a9bffe8] +-- pipe:traces_facets:default:text [2008761a] SELECT - ServiceName AS name, - count() AS count, + name AS name, + sum(count) AS count, 'service' AS facetType + FROM ( +SELECT + ServiceName AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + GROUP BY name +UNION ALL +SELECT + ServiceName AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + GROUP BY name +) AS service_tiers GROUP BY name ORDER BY count DESC LIMIT 50 UNION ALL SELECT - SpanName AS name, + name AS name, + sum(count) AS count, + 'spanName' AS facetType + FROM ( +SELECT + SpanName AS name, + count() AS count + FROM trace_list_mv + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND SpanName != '' + GROUP BY name +UNION ALL +SELECT + SpanName AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND SpanName != '' + GROUP BY name +) AS spanName_tiers + GROUP BY name + ORDER BY count DESC + LIMIT 20 +UNION ALL +SELECT + name AS name, + sum(count) AS count, + 'httpMethod' AS facetType + FROM ( +SELECT + HttpMethod AS name, + count() AS count + FROM trace_list_mv + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND HttpMethod != '' + GROUP BY name +UNION ALL +SELECT + HttpMethod AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND HttpMethod != '' + GROUP BY name +) AS httpMethod_tiers + GROUP BY name + ORDER BY count DESC + LIMIT 20 +UNION ALL +SELECT + name AS name, + sum(count) AS count, + 'httpStatus' AS facetType + FROM ( +SELECT + HttpStatusCode AS name, + count() AS count + FROM trace_list_mv + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND HttpStatusCode != '' + GROUP BY name +UNION ALL +SELECT + HttpStatusCode AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND HttpStatusCode != '' + GROUP BY name +) AS httpStatus_tiers + GROUP BY name + ORDER BY count DESC + LIMIT 20 +UNION ALL +SELECT + name AS name, + sum(count) AS count, + 'deploymentEnv' AS facetType + FROM ( +SELECT + DeploymentEnv AS name, + count() AS count + FROM trace_list_mv + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND DeploymentEnv != '' + GROUP BY name +UNION ALL +SELECT + DeploymentEnv AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND DeploymentEnv != '' + GROUP BY name +) AS deploymentEnv_tiers + GROUP BY name + ORDER BY count DESC + LIMIT 20 +UNION ALL +SELECT + name AS name, + sum(count) AS count, + 'serviceNamespace' AS facetType + FROM ( +SELECT + ServiceNamespace AS name, + count() AS count + FROM trace_list_mv + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND ServiceNamespace != '' + GROUP BY name +UNION ALL +SELECT + ServiceNamespace AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND ServiceNamespace != '' + GROUP BY name +) AS serviceNamespace_tiers + GROUP BY name + ORDER BY count DESC + LIMIT 20 +UNION ALL +SELECT + 'error' AS name, + sum(count) AS count, + 'errorCount' AS facetType + FROM ( +SELECT + count() AS count + FROM trace_list_mv + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND HasError = 1 +UNION ALL +SELECT + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND HasError = 1 +) AS errorCount_tiers +FORMAT JSON + +-- pipe:traces_facets:duration-bounded:baseline [1e990ddf] +SELECT + ServiceName AS name, + count() AS count, + 'service' AS facetType + FROM trace_list_mv + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND Duration >= 100000000 + AND Duration <= 5000000000 + GROUP BY name + ORDER BY count DESC + LIMIT 50 +UNION ALL +SELECT + SpanName AS name, + count() AS count, + 'spanName' AS facetType + FROM trace_list_mv + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND Duration >= 100000000 + AND Duration <= 5000000000 + AND SpanName != '' + GROUP BY name + ORDER BY count DESC + LIMIT 20 +UNION ALL +SELECT + HttpMethod AS name, + count() AS count, + 'httpMethod' AS facetType + FROM trace_list_mv + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND Duration >= 100000000 + AND Duration <= 5000000000 + AND HttpMethod != '' + GROUP BY name + ORDER BY count DESC + LIMIT 20 +UNION ALL +SELECT + HttpStatusCode AS name, + count() AS count, + 'httpStatus' AS facetType + FROM trace_list_mv + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND Duration >= 100000000 + AND Duration <= 5000000000 + AND HttpStatusCode != '' + GROUP BY name + ORDER BY count DESC + LIMIT 20 +UNION ALL +SELECT + DeploymentEnv AS name, + count() AS count, + 'deploymentEnv' AS facetType + FROM trace_list_mv + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND Duration >= 100000000 + AND Duration <= 5000000000 + AND DeploymentEnv != '' + GROUP BY name + ORDER BY count DESC + LIMIT 20 +UNION ALL +SELECT + ServiceNamespace AS name, + count() AS count, + 'serviceNamespace' AS facetType + FROM trace_list_mv + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND Duration >= 100000000 + AND Duration <= 5000000000 + AND ServiceNamespace != '' + GROUP BY name + ORDER BY count DESC + LIMIT 20 +UNION ALL +SELECT + 'error' AS name, + count() AS count, + 'errorCount' AS facetType + FROM trace_list_mv + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND Duration >= 100000000 + AND Duration <= 5000000000 + AND HasError = 1 +FORMAT JSON + +-- pipe:traces_facets:duration-bounded:bloom [1e990ddf] +SELECT + ServiceName AS name, + count() AS count, + 'service' AS facetType + FROM trace_list_mv + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND Duration >= 100000000 + AND Duration <= 5000000000 + GROUP BY name + ORDER BY count DESC + LIMIT 50 +UNION ALL +SELECT + SpanName AS name, + count() AS count, + 'spanName' AS facetType + FROM trace_list_mv + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND Duration >= 100000000 + AND Duration <= 5000000000 + AND SpanName != '' + GROUP BY name + ORDER BY count DESC + LIMIT 20 +UNION ALL +SELECT + HttpMethod AS name, + count() AS count, + 'httpMethod' AS facetType + FROM trace_list_mv + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND Duration >= 100000000 + AND Duration <= 5000000000 + AND HttpMethod != '' + GROUP BY name + ORDER BY count DESC + LIMIT 20 +UNION ALL +SELECT + HttpStatusCode AS name, + count() AS count, + 'httpStatus' AS facetType + FROM trace_list_mv + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND Duration >= 100000000 + AND Duration <= 5000000000 + AND HttpStatusCode != '' + GROUP BY name + ORDER BY count DESC + LIMIT 20 +UNION ALL +SELECT + DeploymentEnv AS name, + count() AS count, + 'deploymentEnv' AS facetType + FROM trace_list_mv + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND Duration >= 100000000 + AND Duration <= 5000000000 + AND DeploymentEnv != '' + GROUP BY name + ORDER BY count DESC + LIMIT 20 +UNION ALL +SELECT + ServiceNamespace AS name, + count() AS count, + 'serviceNamespace' AS facetType + FROM trace_list_mv + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND Duration >= 100000000 + AND Duration <= 5000000000 + AND ServiceNamespace != '' + GROUP BY name + ORDER BY count DESC + LIMIT 20 +UNION ALL +SELECT + 'error' AS name, + count() AS count, + 'errorCount' AS facetType + FROM trace_list_mv + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND Duration >= 100000000 + AND Duration <= 5000000000 + AND HasError = 1 +FORMAT JSON + +-- pipe:traces_facets:duration-bounded:text [1e990ddf] +SELECT + ServiceName AS name, + count() AS count, + 'service' AS facetType + FROM trace_list_mv + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND Duration >= 100000000 + AND Duration <= 5000000000 + GROUP BY name + ORDER BY count DESC + LIMIT 50 +UNION ALL +SELECT + SpanName AS name, count() AS count, 'spanName' AS facetType FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND Duration >= 100000000 + AND Duration <= 5000000000 AND SpanName != '' GROUP BY name ORDER BY count DESC @@ -11176,6 +11864,9 @@ SELECT WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND Duration >= 100000000 + AND Duration <= 5000000000 AND HttpMethod != '' GROUP BY name ORDER BY count DESC @@ -11189,6 +11880,9 @@ SELECT WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND Duration >= 100000000 + AND Duration <= 5000000000 AND HttpStatusCode != '' GROUP BY name ORDER BY count DESC @@ -11202,6 +11896,9 @@ SELECT WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND Duration >= 100000000 + AND Duration <= 5000000000 AND DeploymentEnv != '' GROUP BY name ORDER BY count DESC @@ -11215,6 +11912,9 @@ SELECT WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND Duration >= 100000000 + AND Duration <= 5000000000 AND ServiceNamespace != '' GROUP BY name ORDER BY count DESC @@ -11228,6 +11928,9 @@ SELECT WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND Duration >= 100000000 + AND Duration <= 5000000000 AND HasError = 1 FORMAT JSON @@ -12293,330 +12996,733 @@ SELECT LIMIT 10 FORMAT JSON --- spec:traces-facets-single-dimension:baseline [a277cb3c] +-- spec:traces-facets-single-dimension:baseline [e072f533] SELECT - SpanName AS name, - count() AS count, + name AS name, + sum(count) AS count, 'spanName' AS facetType + FROM ( +SELECT + SpanName AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' AND ServiceName = 'api' AND DeploymentEnv = 'production' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND SpanName != '' + GROUP BY name +UNION ALL +SELECT + SpanName AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND ServiceName = 'api' + AND DeploymentEnv = 'production' AND SpanName != '' GROUP BY name +) AS spanName_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 FORMAT JSON --- spec:traces-facets:baseline [eabfe99e] +-- spec:traces-facets:baseline [eb366b9e] SELECT - ServiceName AS name, - count() AS count, + name AS name, + sum(count) AS count, 'service' AS facetType + FROM ( +SELECT + ServiceName AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' AND ServiceName = 'api' AND DeploymentEnv = 'production' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + GROUP BY name +UNION ALL +SELECT + ServiceName AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND ServiceName = 'api' + AND DeploymentEnv = 'production' + GROUP BY name +) AS service_tiers GROUP BY name ORDER BY count DESC LIMIT 50 UNION ALL SELECT - SpanName AS name, - count() AS count, + name AS name, + sum(count) AS count, 'spanName' AS facetType + FROM ( +SELECT + SpanName AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' AND ServiceName = 'api' AND DeploymentEnv = 'production' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND SpanName != '' + GROUP BY name +UNION ALL +SELECT + SpanName AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND ServiceName = 'api' + AND DeploymentEnv = 'production' AND SpanName != '' GROUP BY name +) AS spanName_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - HttpMethod AS name, - count() AS count, + name AS name, + sum(count) AS count, 'httpMethod' AS facetType + FROM ( +SELECT + HttpMethod AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' AND ServiceName = 'api' AND DeploymentEnv = 'production' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND HttpMethod != '' + GROUP BY name +UNION ALL +SELECT + HttpMethod AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND ServiceName = 'api' + AND DeploymentEnv = 'production' AND HttpMethod != '' GROUP BY name +) AS httpMethod_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - HttpStatusCode AS name, - count() AS count, + name AS name, + sum(count) AS count, 'httpStatus' AS facetType + FROM ( +SELECT + HttpStatusCode AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' AND ServiceName = 'api' AND DeploymentEnv = 'production' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND HttpStatusCode != '' + GROUP BY name +UNION ALL +SELECT + HttpStatusCode AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND ServiceName = 'api' + AND DeploymentEnv = 'production' AND HttpStatusCode != '' GROUP BY name +) AS httpStatus_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - DeploymentEnv AS name, - count() AS count, + name AS name, + sum(count) AS count, 'deploymentEnv' AS facetType + FROM ( +SELECT + DeploymentEnv AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' AND ServiceName = 'api' AND DeploymentEnv = 'production' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND DeploymentEnv != '' + GROUP BY name +UNION ALL +SELECT + DeploymentEnv AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND ServiceName = 'api' + AND DeploymentEnv = 'production' AND DeploymentEnv != '' GROUP BY name +) AS deploymentEnv_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - ServiceNamespace AS name, - count() AS count, + name AS name, + sum(count) AS count, 'serviceNamespace' AS facetType + FROM ( +SELECT + ServiceNamespace AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' AND ServiceName = 'api' AND DeploymentEnv = 'production' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND ServiceNamespace != '' + GROUP BY name +UNION ALL +SELECT + ServiceNamespace AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND ServiceName = 'api' + AND DeploymentEnv = 'production' AND ServiceNamespace != '' GROUP BY name +) AS serviceNamespace_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT 'error' AS name, - count() AS count, + sum(count) AS count, 'errorCount' AS facetType + FROM ( +SELECT + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' AND ServiceName = 'api' AND DeploymentEnv = 'production' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND HasError = 1 +UNION ALL +SELECT + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND ServiceName = 'api' + AND DeploymentEnv = 'production' AND HasError = 1 +) AS errorCount_tiers FORMAT JSON --- spec:traces-facets:bloom [eabfe99e] +-- spec:traces-facets:bloom [eb366b9e] +SELECT + name AS name, + sum(count) AS count, + 'service' AS facetType + FROM ( +SELECT + ServiceName AS name, + count() AS count + FROM trace_list_mv + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND DeploymentEnv = 'production' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + GROUP BY name +UNION ALL SELECT ServiceName AS name, - count() AS count, - 'service' AS facetType - FROM trace_list_mv + sum(TraceCount) AS count + FROM trace_facets_hourly WHERE OrgId = 'org_sql_catalog' - AND Timestamp >= '2026-01-01 10:30:00' - AND Timestamp <= '2026-01-03 14:15:00' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) AND ServiceName = 'api' AND DeploymentEnv = 'production' GROUP BY name +) AS service_tiers + GROUP BY name ORDER BY count DESC LIMIT 50 UNION ALL SELECT - SpanName AS name, - count() AS count, + name AS name, + sum(count) AS count, 'spanName' AS facetType + FROM ( +SELECT + SpanName AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' AND ServiceName = 'api' AND DeploymentEnv = 'production' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND SpanName != '' + GROUP BY name +UNION ALL +SELECT + SpanName AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND ServiceName = 'api' + AND DeploymentEnv = 'production' AND SpanName != '' GROUP BY name +) AS spanName_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - HttpMethod AS name, - count() AS count, + name AS name, + sum(count) AS count, 'httpMethod' AS facetType + FROM ( +SELECT + HttpMethod AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' AND ServiceName = 'api' AND DeploymentEnv = 'production' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND HttpMethod != '' + GROUP BY name +UNION ALL +SELECT + HttpMethod AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND ServiceName = 'api' + AND DeploymentEnv = 'production' AND HttpMethod != '' GROUP BY name +) AS httpMethod_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - HttpStatusCode AS name, - count() AS count, + name AS name, + sum(count) AS count, 'httpStatus' AS facetType + FROM ( +SELECT + HttpStatusCode AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' AND ServiceName = 'api' AND DeploymentEnv = 'production' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND HttpStatusCode != '' + GROUP BY name +UNION ALL +SELECT + HttpStatusCode AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND ServiceName = 'api' + AND DeploymentEnv = 'production' AND HttpStatusCode != '' GROUP BY name +) AS httpStatus_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - DeploymentEnv AS name, - count() AS count, + name AS name, + sum(count) AS count, 'deploymentEnv' AS facetType + FROM ( +SELECT + DeploymentEnv AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' AND ServiceName = 'api' AND DeploymentEnv = 'production' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND DeploymentEnv != '' + GROUP BY name +UNION ALL +SELECT + DeploymentEnv AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND ServiceName = 'api' + AND DeploymentEnv = 'production' AND DeploymentEnv != '' GROUP BY name +) AS deploymentEnv_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - ServiceNamespace AS name, - count() AS count, + name AS name, + sum(count) AS count, 'serviceNamespace' AS facetType + FROM ( +SELECT + ServiceNamespace AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' AND ServiceName = 'api' AND DeploymentEnv = 'production' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND ServiceNamespace != '' + GROUP BY name +UNION ALL +SELECT + ServiceNamespace AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND ServiceName = 'api' + AND DeploymentEnv = 'production' AND ServiceNamespace != '' GROUP BY name +) AS serviceNamespace_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT 'error' AS name, - count() AS count, + sum(count) AS count, 'errorCount' AS facetType + FROM ( +SELECT + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' AND ServiceName = 'api' AND DeploymentEnv = 'production' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND HasError = 1 +UNION ALL +SELECT + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND ServiceName = 'api' + AND DeploymentEnv = 'production' AND HasError = 1 +) AS errorCount_tiers FORMAT JSON --- spec:traces-facets:text [eabfe99e] +-- spec:traces-facets:text [eb366b9e] SELECT - ServiceName AS name, - count() AS count, + name AS name, + sum(count) AS count, 'service' AS facetType + FROM ( +SELECT + ServiceName AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' AND ServiceName = 'api' AND DeploymentEnv = 'production' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + GROUP BY name +UNION ALL +SELECT + ServiceName AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND ServiceName = 'api' + AND DeploymentEnv = 'production' + GROUP BY name +) AS service_tiers GROUP BY name ORDER BY count DESC LIMIT 50 UNION ALL SELECT - SpanName AS name, - count() AS count, + name AS name, + sum(count) AS count, 'spanName' AS facetType + FROM ( +SELECT + SpanName AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' AND ServiceName = 'api' AND DeploymentEnv = 'production' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND SpanName != '' + GROUP BY name +UNION ALL +SELECT + SpanName AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND ServiceName = 'api' + AND DeploymentEnv = 'production' AND SpanName != '' GROUP BY name +) AS spanName_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - HttpMethod AS name, - count() AS count, + name AS name, + sum(count) AS count, 'httpMethod' AS facetType + FROM ( +SELECT + HttpMethod AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' AND ServiceName = 'api' AND DeploymentEnv = 'production' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND HttpMethod != '' + GROUP BY name +UNION ALL +SELECT + HttpMethod AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND ServiceName = 'api' + AND DeploymentEnv = 'production' AND HttpMethod != '' GROUP BY name +) AS httpMethod_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - HttpStatusCode AS name, - count() AS count, + name AS name, + sum(count) AS count, 'httpStatus' AS facetType + FROM ( +SELECT + HttpStatusCode AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' AND ServiceName = 'api' AND DeploymentEnv = 'production' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND HttpStatusCode != '' + GROUP BY name +UNION ALL +SELECT + HttpStatusCode AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND ServiceName = 'api' + AND DeploymentEnv = 'production' AND HttpStatusCode != '' GROUP BY name +) AS httpStatus_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - DeploymentEnv AS name, - count() AS count, + name AS name, + sum(count) AS count, 'deploymentEnv' AS facetType + FROM ( +SELECT + DeploymentEnv AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' AND ServiceName = 'api' AND DeploymentEnv = 'production' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND DeploymentEnv != '' + GROUP BY name +UNION ALL +SELECT + DeploymentEnv AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND ServiceName = 'api' + AND DeploymentEnv = 'production' AND DeploymentEnv != '' GROUP BY name +) AS deploymentEnv_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - ServiceNamespace AS name, - count() AS count, + name AS name, + sum(count) AS count, 'serviceNamespace' AS facetType + FROM ( +SELECT + ServiceNamespace AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' AND ServiceName = 'api' AND DeploymentEnv = 'production' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND ServiceNamespace != '' + GROUP BY name +UNION ALL +SELECT + ServiceNamespace AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND ServiceName = 'api' + AND DeploymentEnv = 'production' AND ServiceNamespace != '' GROUP BY name +) AS serviceNamespace_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT 'error' AS name, - count() AS count, + sum(count) AS count, 'errorCount' AS facetType + FROM ( +SELECT + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' AND ServiceName = 'api' AND DeploymentEnv = 'production' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND HasError = 1 +UNION ALL +SELECT + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND ServiceName = 'api' + AND DeploymentEnv = 'production' AND HasError = 1 +) AS errorCount_tiers FORMAT JSON -- spec:traces-list-grouped-attr-fallback:baseline [6b9a4329] @@ -13001,18 +14107,38 @@ SELECT LIMIT 50 FORMAT JSON --- spec:traces-stats:baseline [333e79e5] +-- spec:traces-stats:baseline [f262a535] SELECT - min(Duration) / 1000000 AS minDurationMs, - max(Duration) / 1000000 AS maxDurationMs, - ifNull(ifNotFinite(quantile(0.5)(Duration) / 1000000, 0), 0) AS p50DurationMs, - ifNull(ifNotFinite(quantile(0.95)(Duration) / 1000000, 0), 0) AS p95DurationMs + minIf(durationMin, traceCount > 0) / 1000000 AS minDurationMs, + maxIf(durationMax, traceCount > 0) / 1000000 AS maxDurationMs, + ifNull(ifNotFinite(arrayElement(quantilesTDigestMerge(0.5, 0.95)(durationQuantiles), 1) / 1000000, 0), 0) AS p50DurationMs, + ifNull(ifNotFinite(arrayElement(quantilesTDigestMerge(0.5, 0.95)(durationQuantiles), 2) / 1000000, 0), 0) AS p95DurationMs + FROM ( +SELECT + count() AS traceCount, + min(Duration) AS durationMin, + max(Duration) AS durationMax, + quantilesTDigestState(0.5, 0.95)(Duration) AS durationQuantiles FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' AND ServiceName = 'api' AND DeploymentEnv = 'production' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) +UNION ALL +SELECT + sum(TraceCount) AS traceCount, + min(DurationMin) AS durationMin, + max(DurationMax) AS durationMax, + quantilesTDigestMergeState(0.5, 0.95)(DurationQuantiles) AS durationQuantiles + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND ServiceName = 'api' + AND DeploymentEnv = 'production' +) AS duration_tiers FORMAT JSON -- spec:traces-timeseries-aggregates-mv:baseline [cad07ec0] diff --git a/packages/query-engine/src/benchmark/catalog.ts b/packages/query-engine/src/benchmark/catalog.ts index 8c6473b147..08b5c8fd71 100644 --- a/packages/query-engine/src/benchmark/catalog.ts +++ b/packages/query-engine/src/benchmark/catalog.ts @@ -146,7 +146,14 @@ export const pipeFixtures: ReadonlyArray = [ params: { trace_id: TRACE_ID, start_time: undefined, end_time: undefined }, }, { pipe: "traces_duration_stats", label: "default", params: {} }, + { pipe: "traces_duration_stats", label: "duration-bounded", params: { min_duration_ms: 100 } }, { pipe: "traces_facets", label: "default", params: {}, allCapabilities: true }, + { + pipe: "traces_facets", + label: "duration-bounded", + params: { service: "api", min_duration_ms: 100, max_duration_ms: 5000 }, + allCapabilities: true, + }, { pipe: "traces_facets", label: "attribute-filtered", @@ -1187,7 +1194,7 @@ export function undecodedColumns( */ const ROLLUP_TABLE_RE = /\w_(?:hourly|minutely|daily)\b|\w_aggregates_\w/ const RAW_TABLE_RE = - /\bFROM\s+(?:traces|logs|service_map_spans|service_map_children|service_overview_spans)\b/ + /\bFROM\s+(?:traces|logs|service_map_spans|service_map_children|service_overview_spans|trace_list_mv)\b/ /** * The `firstFullBucket` fragment `makeGrain` emits. Structural rather than a @@ -1276,6 +1283,22 @@ export function routeCoverage(): ReadonlyMap + record( + "canUseTraceFacetsRollup", + CH.canUseTraceFacetsRollup({ + minDurationMs: params.min_duration_ms as number | undefined, + maxDurationMs: params.max_duration_ms as number | undefined, + attributeFilterKey: params.attribute_filter_key as string | undefined, + resourceFilterKey: params.resource_filter_key as string | undefined, + }), + ) + } + // `useProductEvents` picks the page-view source: the `product_events` rollup, or raw // `session_events`. Read off the fixture labels rather than re-deriving it — // `webAnalyticsVariants` emits every web-analytics fixture in both variants diff --git a/packages/query-engine/src/ch/ch.test.ts b/packages/query-engine/src/ch/ch.test.ts index b7bcde54f2..c97b61da80 100644 --- a/packages/query-engine/src/ch/ch.test.ts +++ b/packages/query-engine/src/ch/ch.test.ts @@ -1332,8 +1332,8 @@ describe("converted queries", () => { }) const { sql } = compileUnsafe(q, baseParams) expect(sql).toContain("positionCaseInsensitive(ServiceName, 'api') > 0") - expect(sql).toContain("quantile(0.5)(Duration)") - expect(sql).toContain("FROM trace_list_mv") // tracesDurationStats always uses MV directly + expect(sql).toContain("quantilesTDigestMergeState(0.5, 0.95)(DurationQuantiles)") + expect(sql).toContain("FROM trace_facets_hourly") }) it("spanHierarchyQuery projects only the trimmed tree attribute keys", () => { diff --git a/packages/query-engine/src/ch/index.ts b/packages/query-engine/src/ch/index.ts index 0c7e069908..84c093d07e 100644 --- a/packages/query-engine/src/ch/index.ts +++ b/packages/query-engine/src/ch/index.ts @@ -334,6 +334,7 @@ export { spanDetailQuery, traceTimeProbeQuery, tracesDurationStatsQuery, + canUseTraceFacetsRollup, tracesFacetsQuery, errorsFacetsQuery, errorsSummaryQuery, diff --git a/packages/query-engine/src/ch/queries/errors.test.ts b/packages/query-engine/src/ch/queries/errors.test.ts index 5bacdcd42c..bc3ae9537c 100644 --- a/packages/query-engine/src/ch/queries/errors.test.ts +++ b/packages/query-engine/src/ch/queries/errors.test.ts @@ -11,6 +11,7 @@ import { errorTickBootstrapIssuesQuery, errorTickIssuesQuery, errorFingerprintsQuery, + tracesDurationStatsQuery, tracesFacetsQuery, } from "./errors" @@ -483,8 +484,9 @@ describe("tracesFacetsQuery", () => { it("compiles UNION ALL with 7 facet dimensions", () => { const q = tracesFacetsQuery({}) const { sql } = compileUnionUnsafe(q, baseParams) - const unionCount = (sql.match(/UNION ALL/g) || []).length - expect(unionCount).toBe(6) // 7 queries = 6 UNION ALL + // 7 facet branches, each splicing a raw edge with the hourly interior. + expect(sql.match(/\) AS \w+_tiers/g)).toHaveLength(7) + expect(sql.match(/UNION ALL/g)).toHaveLength(6 + 7) expect(sql).toContain("'service' AS facetType") expect(sql).toContain("'spanName' AS facetType") expect(sql).toContain("'httpMethod' AS facetType") @@ -546,7 +548,8 @@ describe("tracesFacetsQuery", () => { it("compiles only the requested branch when facet is set", () => { const q = tracesFacetsQuery({ facet: "service" }) const { sql } = compileUnionUnsafe(q, baseParams) - expect(sql).not.toContain("UNION ALL") + // The only UNION left is the branch's own two tiers. + expect(sql.match(/UNION ALL/g)).toHaveLength(1) expect(sql).toContain("'service' AS facetType") expect(sql).not.toContain("'spanName' AS facetType") expect(sql).not.toContain("'errorCount' AS facetType") @@ -558,9 +561,59 @@ describe("tracesFacetsQuery", () => { it("keeps the non-service branch empty-value guard when facet-scoped", () => { const q = tracesFacetsQuery({ facet: "deploymentEnv" }) const { sql } = compileUnionUnsafe(q, baseParams) - expect(sql).not.toContain("UNION ALL") + expect(sql.match(/UNION ALL/g)).toHaveLength(1) expect(sql).toContain("'deploymentEnv' AS facetType") - expect(sql).toContain("DeploymentEnv != ''") + // On both tiers. + expect(sql.match(/DeploymentEnv != ''/g)).toHaveLength(2) expect(sql).toContain("LIMIT 20") }) }) + +describe("trace facets rollup routing", () => { + const firstFullHour = "toStartOfHour(toDateTime('2024-01-01 00:00:00')) + INTERVAL 1 HOUR" + const endHour = "toStartOfHour(toDateTime('2024-01-02 00:00:00'))" + + it("reads whole hours from trace_facets_hourly and the partial ends from trace_list_mv", () => { + const filters = { serviceNames: ["api", "web"], hasError: true, deploymentEnvs: ["production"] } + for (const sql of [ + compileUnionUnsafe(tracesFacetsQuery({ ...filters, facet: "spanName" }), baseParams).sql, + compileUnsafe(tracesDurationStatsQuery(filters), baseParams).sql, + ]) { + expect(sql).toContain("FROM trace_facets_hourly") + expect(sql).toContain("FROM trace_list_mv") + // The rollup-splice boundary on both sides. + expect(sql).toContain(`Hour >= if(`) + expect(sql).toContain(`${firstFullHour})`) + expect(sql).toContain(`Hour < ${endHour}`) + expect(sql).toContain(`OR Timestamp >= ${endHour})`) + // Every filter reaches both tiers. + expect(sql.match(/ServiceName IN \('api', 'web'\)/g)).toHaveLength(2) + expect(sql.match(/HasError = 1/g)).toHaveLength(2) + expect(sql.match(/DeploymentEnv = 'production'/g)).toHaveLength(2) + } + }) + + it("merges duration state across the tiers, ignoring an empty tier's extremes", () => { + const { sql } = compileUnsafe(tracesDurationStatsQuery({}), baseParams) + expect(sql).toContain("quantilesTDigestState(0.5, 0.95)(Duration)") + expect(sql).toContain("quantilesTDigestMergeState(0.5, 0.95)(DurationQuantiles)") + expect(sql).toContain("minIf(durationMin, traceCount > 0)") + expect(sql).toContain("maxIf(durationMax, traceCount > 0)") + }) + + it("reads only trace_list_mv for filters the rollup does not carry", () => { + for (const opts of [ + { minDurationMs: 100 }, + { maxDurationMs: 100 }, + { attributeFilterKey: "http.route", attributeFilterValue: "/users" }, + { resourceFilterKey: "host.name", resourceFilterValue: "web-1" }, + ]) { + const facets = compileUnionUnsafe(tracesFacetsQuery(opts), baseParams).sql + expect(facets, JSON.stringify(opts)).not.toContain("trace_facets_hourly") + expect(facets.match(/UNION ALL/g), JSON.stringify(opts)).toHaveLength(6) + } + const stats = compileUnsafe(tracesDurationStatsQuery({ minDurationMs: 100 }), baseParams).sql + expect(stats).not.toContain("trace_facets_hourly") + expect(stats).toContain("quantile(0.5)(Duration)") + }) +}) diff --git a/packages/query-engine/src/ch/queries/errors.ts b/packages/query-engine/src/ch/queries/errors.ts index 4bd6268080..ef6dd176a1 100644 --- a/packages/query-engine/src/ch/queries/errors.ts +++ b/packages/query-engine/src/ch/queries/errors.ts @@ -21,6 +21,7 @@ import { ErrorFingerprintsMinutely, ServiceUsage, TraceDetailSpans, + TraceFacetsHourly, TraceListMv, Traces, } from "../tables" @@ -535,7 +536,13 @@ export function traceTimeProbeQuery(opts: { traceId: string; narrowByTime?: bool .format("JSON") } -// Traces duration stats +// Traces duration stats and facets +// +// Both count root spans from `trace_list_mv` over the page's window. That table +// holds ~14M root spans a day for a busy org, so past about a day a scan of it +// exceeds the discovery budget. When the filters are ones `trace_facets_hourly` +// carries, its whole hours answer the interior of the window and +// `trace_list_mv` only the partial hour at each end (`rollup-splice`). export interface TracesDurationStatsOpts { serviceName?: string @@ -565,14 +572,22 @@ export interface TracesDurationStatsOpts { } } -export interface TracesDurationStatsOutput { - readonly minDurationMs: number - readonly maxDurationMs: number - readonly p50DurationMs: number - readonly p95DurationMs: number -} - -export function tracesDurationStatsQuery(opts: TracesDurationStatsOpts) { +/** The facet dimensions, spelled the same on `trace_list_mv` and `trace_facets_hourly`. */ +type TraceFacetColumns = Pick< + typeof TraceListMv.columns, + | "ServiceName" + | "SpanName" + | "HttpMethod" + | "HttpStatusCode" + | "DeploymentEnv" + | "ServiceNamespace" + | "HasError" +> + +function traceFacetDimensionConditions( + $: ColumnAccessor, + opts: TracesDurationStatsOpts, +): Array { const mm = opts.matchModes const services = inclusionValues(opts.serviceName, opts.serviceNames) const spanNames = inclusionValues(opts.spanName, opts.spanNames) @@ -581,39 +596,126 @@ export function tracesDurationStatsQuery(opts: TracesDurationStatsOpts) { const envs = inclusionValues(opts.deploymentEnv, opts.deploymentEnvs) const namespaces = inclusionValues(opts.namespace, opts.namespaces) - return from(TraceListMv) + return [ + CH.when(services, (v: readonly string[]) => + matchOrIn($.ServiceName, v, mm?.serviceName === "contains"), + ), + CH.when(spanNames, (v: readonly string[]) => matchOrIn($.SpanName, v, mm?.spanName === "contains")), + CH.whenTrue(!!opts.hasError, () => $.HasError.eq(1)), + CH.when(httpMethods, (v: readonly string[]) => inclusionCondition($.HttpMethod, v)), + CH.when(httpStatusCodes, (v: readonly string[]) => inclusionCondition($.HttpStatusCode, v)), + CH.when(envs, (v: readonly string[]) => + matchOrIn($.DeploymentEnv, v, mm?.deploymentEnv === "contains"), + ), + CH.when(namespaces, (v: readonly string[]) => + matchOrIn($.ServiceNamespace, v, mm?.serviceNamespace === "contains"), + ), + ] +} + +function traceListWindowConditions( + $: ColumnAccessor, + opts: TracesDurationStatsOpts, +) { + return [ + $.OrgId.eq(param.string("orgId")), + $.Timestamp.gte(param.dateTimeSeconds("startTime")), + $.Timestamp.lte(param.dateTimeSeconds("endTime")), + ...traceFacetDimensionConditions($, opts), + CH.when(opts.minDurationMs, (v: number) => $.Duration.gte(v * 1000000)), + CH.when(opts.maxDurationMs, (v: number) => $.Duration.lte(v * 1000000)), + ] +} + +function traceFacetsHourlyInteriorConditions( + $: ColumnAccessor, + opts: TracesDurationStatsOpts, +) { + return [ + $.OrgId.eq(param.string("orgId")), + ...interiorConditions($.Hour), + ...traceFacetDimensionConditions($, opts), + ] +} + +/** + * Whether `trace_facets_hourly` can answer the whole-hour interior. It keeps the + * facet dimensions and nothing finer, so a duration bound or an attribute + * filter, which each need the individual root span, reads `trace_list_mv` for + * the whole window. + */ +export function canUseTraceFacetsRollup(opts: { + readonly minDurationMs?: number + readonly maxDurationMs?: number + readonly attributeFilterKey?: string + readonly resourceFilterKey?: string +}): boolean { + return ( + opts.minDurationMs == null && + opts.maxDurationMs == null && + !opts.attributeFilterKey && + !opts.resourceFilterKey + ) +} + +export interface TracesDurationStatsOutput { + readonly minDurationMs: number + readonly maxDurationMs: number + readonly p50DurationMs: number + readonly p95DurationMs: number +} + +export function tracesDurationStatsQuery( + opts: TracesDurationStatsOpts, +): CHQuery { + if (!canUseTraceFacetsRollup(opts)) { + return from(TraceListMv) + .select(($) => ({ + minDurationMs: CH.min_($.Duration).div(1000000), + maxDurationMs: CH.max_($.Duration).div(1000000), + p50DurationMs: finiteOrZero(CH.quantile(0.5)($.Duration).div(1000000)), + p95DurationMs: finiteOrZero(CH.quantile(0.95)($.Duration).div(1000000)), + })) + .where(($) => traceListWindowConditions($, opts)) + .format("JSON") + } + + // Each tier reports its row count beside its extremes: an aggregate over an + // empty tier returns 0 rather than nothing, and that 0 must not win the + // outer `min`. The t-digest states merge across tiers; an empty one is inert. + const rawEdges = from(TraceListMv) .select(($) => ({ - minDurationMs: CH.min_($.Duration).div(1000000), - maxDurationMs: CH.max_($.Duration).div(1000000), - p50DurationMs: finiteOrZero(CH.quantile(0.5)($.Duration).div(1000000)), - p95DurationMs: finiteOrZero(CH.quantile(0.95)($.Duration).div(1000000)), + traceCount: CH.count(), + durationMin: CH.min_($.Duration), + durationMax: CH.max_($.Duration), + durationQuantiles: CH.rawExpr("quantilesTDigestState(0.5, 0.95)(Duration)", T.string), })) - .where(($) => [ - $.OrgId.eq(param.string("orgId")), - $.Timestamp.gte(param.dateTimeSeconds("startTime")), - $.Timestamp.lte(param.dateTimeSeconds("endTime")), - CH.when(services, (v: readonly string[]) => - matchOrIn($.ServiceName, v, mm?.serviceName === "contains"), - ), - CH.when(spanNames, (v: readonly string[]) => - matchOrIn($.SpanName, v, mm?.spanName === "contains"), - ), - CH.whenTrue(!!opts.hasError, () => $.HasError.eq(1)), - CH.when(opts.minDurationMs, (v: number) => $.Duration.gte(v * 1000000)), - CH.when(opts.maxDurationMs, (v: number) => $.Duration.lte(v * 1000000)), - CH.when(httpMethods, (v: readonly string[]) => inclusionCondition($.HttpMethod, v)), - CH.when(httpStatusCodes, (v: readonly string[]) => inclusionCondition($.HttpStatusCode, v)), - CH.when(envs, (v: readonly string[]) => - matchOrIn($.DeploymentEnv, v, mm?.deploymentEnv === "contains"), - ), - CH.when(namespaces, (v: readonly string[]) => - matchOrIn($.ServiceNamespace, v, mm?.serviceNamespace === "contains"), + .where(($) => [...traceListWindowConditions($, opts), edgeCondition("Timestamp")]) + + const hourlyInterior = from(TraceFacetsHourly) + .select(($) => ({ + traceCount: CH.sum($.TraceCount), + durationMin: CH.min_($.DurationMin), + durationMax: CH.max_($.DurationMax), + durationQuantiles: CH.rawExpr( + "quantilesTDigestMergeState(0.5, 0.95)(DurationQuantiles)", + T.string, ), - ]) + })) + .where(($) => traceFacetsHourlyInteriorConditions($, opts)) + + const quantiles = "quantilesTDigestMerge(0.5, 0.95)(durationQuantiles)" + return fromUnion(unionAll(rawEdges, hourlyInterior), "duration_tiers") + .select(() => ({ + minDurationMs: CH.rawExpr("minIf(durationMin, traceCount > 0) / 1000000", T.float64), + maxDurationMs: CH.rawExpr("maxIf(durationMax, traceCount > 0) / 1000000", T.float64), + p50DurationMs: finiteOrZero(CH.rawExpr(`arrayElement(${quantiles}, 1) / 1000000`, T.float64)), + p95DurationMs: finiteOrZero(CH.rawExpr(`arrayElement(${quantiles}, 2) / 1000000`, T.float64)), + })) .format("JSON") } -// Traces facets (UNION ALL — 6 facet dimensions on trace_list_mv) +// Traces facets (UNION ALL — 6 facet dimensions and the error count) export type TracesFacetDimension = | "service" @@ -623,32 +725,7 @@ export type TracesFacetDimension = | "deploymentEnv" | "serviceNamespace" -export interface TracesFacetsOpts { - serviceName?: string - spanName?: string - hasError?: boolean - minDurationMs?: number - maxDurationMs?: number - httpMethod?: string - httpStatusCode?: string - deploymentEnv?: string - namespace?: string - /** - * Multi-value spellings, compiled to `IN (...)` against trace_list_mv's - * pre-extracted columns. Each wins over its scalar counterpart when non-empty. - */ - serviceNames?: readonly string[] - spanNames?: readonly string[] - httpMethods?: readonly string[] - httpStatusCodes?: readonly string[] - deploymentEnvs?: readonly string[] - namespaces?: readonly string[] - matchModes?: { - serviceName?: "contains" - spanName?: "contains" - deploymentEnv?: "contains" - serviceNamespace?: "contains" - } +export interface TracesFacetsOpts extends TracesDurationStatsOpts { attributeFilterKey?: string attributeFilterValue?: string attributeFilterValueMatchMode?: "contains" @@ -668,39 +745,8 @@ type StringColumn = { }[keyof Cols & string] export function tracesFacetsQuery(opts: TracesFacetsOpts): CHUnionQuery { - const baseWhere = ($: ColumnAccessor): Array => { - const conditions: Array = [ - $.OrgId.eq(param.string("orgId")), - $.Timestamp.gte(param.dateTimeSeconds("startTime")), - $.Timestamp.lte(param.dateTimeSeconds("endTime")), - ] - - const services = inclusionValues(opts.serviceName, opts.serviceNames) - const spanNames = inclusionValues(opts.spanName, opts.spanNames) - const httpMethods = inclusionValues(opts.httpMethod, opts.httpMethods) - const httpStatusCodes = inclusionValues(opts.httpStatusCode, opts.httpStatusCodes) - const envs = inclusionValues(opts.deploymentEnv, opts.deploymentEnvs) - const namespaces = inclusionValues(opts.namespace, opts.namespaces) - - if (services) { - conditions.push(matchOrIn($.ServiceName, services, opts.matchModes?.serviceName === "contains")) - } - if (spanNames) { - conditions.push(matchOrIn($.SpanName, spanNames, opts.matchModes?.spanName === "contains")) - } - if (opts.hasError) conditions.push($.HasError.eq(1)) - if (opts.minDurationMs != null) conditions.push($.Duration.gte(opts.minDurationMs * 1000000)) - if (opts.maxDurationMs != null) conditions.push($.Duration.lte(opts.maxDurationMs * 1000000)) - if (httpMethods) conditions.push(inclusionCondition($.HttpMethod, httpMethods)) - if (httpStatusCodes) conditions.push(inclusionCondition($.HttpStatusCode, httpStatusCodes)) - if (envs) { - conditions.push(matchOrIn($.DeploymentEnv, envs, opts.matchModes?.deploymentEnv === "contains")) - } - if (namespaces) { - conditions.push( - matchOrIn($.ServiceNamespace, namespaces, opts.matchModes?.serviceNamespace === "contains"), - ) - } + const rawWhere = ($: ColumnAccessor): Array => { + const conditions: Array = traceListWindowConditions($, opts) // Attribute filter EXISTS subqueries (correlated — references outer TraceId) if (opts.attributeFilterKey) { @@ -757,36 +803,89 @@ export function tracesFacetsQuery(opts: TracesFacetsOpts): CHUnionQuery + + // `colName` is a real column of both tiers, so the accessor already knows how // it decodes — naming it as a `dynamicColumn` threw that away and cost every // facet branch its row schema. const makeFacetQuery = ( - colName: StringColumn, + colName: StringColumn & StringColumn, facetType: string, - extraWhere?: ($: ColumnAccessor) => CH.Condition, + dropEmpty: boolean, limit = 50, - ) => - from(TraceListMv) + ): FacetBranch => { + if (!useRollup) { + return from(TraceListMv) + .select(($) => ({ + name: $[colName], + count: CH.count(), + facetType: CH.lit(facetType), + })) + .where(($) => [...rawWhere($), CH.whenTrue(dropEmpty, () => $[colName].neq(""))]) + .groupBy("name") + .orderBy(["count", "desc"]) + .limit(limit) + } + + const rawEdges = from(TraceListMv) + .select(($) => ({ name: $[colName], count: CH.count() })) + .where(($) => [ + ...rawWhere($), + edgeCondition("Timestamp"), + CH.whenTrue(dropEmpty, () => $[colName].neq("")), + ]) + .groupBy("name") + const hourlyInterior = from(TraceFacetsHourly) + .select(($) => ({ name: $[colName], count: CH.sum($.TraceCount) })) + .where(($) => [ + ...traceFacetsHourlyInteriorConditions($, opts), + CH.whenTrue(dropEmpty, () => $[colName].neq("")), + ]) + .groupBy("name") + return fromUnion(unionAll(rawEdges, hourlyInterior), `${facetType}_tiers`) .select(($) => ({ - name: $[colName], - count: CH.count(), + name: $.name, + count: CH.sum($.count), facetType: CH.lit(facetType), })) - .where(($) => [...baseWhere($), extraWhere?.($)]) .groupBy("name") .orderBy(["count", "desc"]) .limit(limit) + } - const facetBranches: Record ReturnType> = { - service: () => makeFacetQuery("ServiceName", "service"), - spanName: () => makeFacetQuery("SpanName", "spanName", ($) => $.SpanName.neq(""), 20), - httpMethod: () => makeFacetQuery("HttpMethod", "httpMethod", ($) => $.HttpMethod.neq(""), 20), - httpStatus: () => makeFacetQuery("HttpStatusCode", "httpStatus", ($) => $.HttpStatusCode.neq(""), 20), - deploymentEnv: () => - makeFacetQuery("DeploymentEnv", "deploymentEnv", ($) => $.DeploymentEnv.neq(""), 20), - serviceNamespace: () => - makeFacetQuery("ServiceNamespace", "serviceNamespace", ($) => $.ServiceNamespace.neq(""), 20), - } satisfies Record ReturnType> + const errorCountQuery = (): FacetBranch => { + if (!useRollup) { + return from(TraceListMv) + .select(() => ({ + name: CH.lit("error"), + count: CH.count(), + facetType: CH.lit("errorCount"), + })) + .where(($) => [...rawWhere($), $.HasError.eq(1)]) + } + + const rawEdges = from(TraceListMv) + .select(() => ({ count: CH.count() })) + .where(($) => [...rawWhere($), edgeCondition("Timestamp"), $.HasError.eq(1)]) + const hourlyInterior = from(TraceFacetsHourly) + .select(($) => ({ count: CH.sum($.TraceCount) })) + .where(($) => [...traceFacetsHourlyInteriorConditions($, opts), $.HasError.eq(1)]) + return fromUnion(unionAll(rawEdges, hourlyInterior), "errorCount_tiers").select(($) => ({ + name: CH.lit("error"), + count: CH.sum($.count), + facetType: CH.lit("errorCount"), + })) + } + + const facetBranches = { + service: () => makeFacetQuery("ServiceName", "service", false), + spanName: () => makeFacetQuery("SpanName", "spanName", true, 20), + httpMethod: () => makeFacetQuery("HttpMethod", "httpMethod", true, 20), + httpStatus: () => makeFacetQuery("HttpStatusCode", "httpStatus", true, 20), + deploymentEnv: () => makeFacetQuery("DeploymentEnv", "deploymentEnv", true, 20), + serviceNamespace: () => makeFacetQuery("ServiceNamespace", "serviceNamespace", true, 20), + } satisfies Record FacetBranch> if (opts.facet) { return unionAll(facetBranches[opts.facet]()).format("JSON") @@ -799,13 +898,7 @@ export function tracesFacetsQuery(opts: TracesFacetsOpts): CHUnionQuery ({ - name: CH.lit("error"), - count: CH.count(), - facetType: CH.lit("errorCount"), - })) - .where(($) => [...baseWhere($), $.HasError.eq(1)]), + errorCountQuery(), ).format("JSON") } diff --git a/packages/query-engine/src/ch/tables.ts b/packages/query-engine/src/ch/tables.ts index 0a76923c1e..239c6e2d3f 100644 --- a/packages/query-engine/src/ch/tables.ts +++ b/packages/query-engine/src/ch/tables.ts @@ -190,6 +190,24 @@ export const TraceListMv = table("trace_list_mv", { TraceState: T.string, }) +export const TraceFacetsHourly = table("trace_facets_hourly", { + OrgId: orgId, + Hour: dateTime, + ServiceName: T.string, + SpanName: T.string, + HttpMethod: T.string, + HttpStatusCode: T.string, + DeploymentEnv: T.string, + ServiceNamespace: T.string, + HasError: T.uint8, + TraceCount: T.uint64, + DurationMin: T.uint64, + DurationMax: T.uint64, + // AggregateFunction(quantilesTDigest(0.5, 0.95), UInt64) — opaque state, only + // ever touched via raw `...MergeState`/`...Merge` exprs. + DurationQuantiles: T.string, +}) + export const Logs = table("logs", { OrgId: orgId, Timestamp: dateTime64, From 5b4843c82ac067a9e6629d0cfcdb23287704ea4f Mon Sep 17 00:00:00 2001 From: JeremyFunk Date: Mon, 28 Sep 2026 10:25:49 +0200 Subject: [PATCH 3/7] fix(traces): align trace_facets_hourly TTL with its source, backfill locally - TTL is Hour + 30 days, not toDate(Hour) + 30 days: a day's hours no longer expire up to a day before the trace_list_mv rows they summarize. - The local v23 -> v24 edge backfills the rollup from trace_list_mv, so an upgraded local store does not read empty whole hours for 30 days. - The rollup e2e now runs in CI, and checks the live view and the 0034 backfill against trace_list_mv in one pass. - Drop the warehouse catalog note and trim comments and tautological migration asserts. --- .github/workflows/ci.yml | 11 ++ apps/cli/src/server/local-schema-history.ts | 10 +- .../server/local-store-migrations/steps.ts | 16 +- apps/cli/src/server/schema/local-inserts.json | 2 +- .../src/server/schema/local-schema-v24.sql | 2 +- apps/cli/src/server/schema/local-schema.sql | 4 +- apps/cli/test/local-store-migrations.test.ts | 7 +- apps/cli/test/native-local-store-migration.sh | 2 +- apps/ingest/src/clickhouse_insert_mappings.rs | 2 +- ...rly-materialization.clickhouse.e2e.test.ts | 150 +++++------------- .../services/warehouse/warehouse-catalog.ts | 5 - .../migrations/0034_trace_facets_hourly.ts | 21 +-- .../src/clickhouse/migrations/index.test.ts | 11 +- .../domain/src/generated/clickhouse-schema.ts | 4 +- .../generated/tinybird-project-manifest.ts | 4 +- packages/domain/src/tinybird/datasources.ts | 19 +-- .../domain/src/tinybird/materializations.ts | 7 +- 17 files changed, 89 insertions(+), 188 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b3107cb00..c92104aa74 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -523,6 +523,17 @@ jobs: bun run --filter=@maple/backend test -- src/services/warehouse/web-analytics-parity.clickhouse.e2e.test.ts + # A cascaded rollup that never fires reads as "no data", not as an error. + - name: Verify the trace facets rollup + env: + CLICKHOUSE_E2E: "1" + CLICKHOUSE_E2E_URL: http://127.0.0.1:8123 + CLICKHOUSE_E2E_USER: maple + CLICKHOUSE_E2E_PASSWORD: maple + run: >- + bun run --filter=@maple/backend test -- + src/services/warehouse/trace-facets-hourly-materialization.clickhouse.e2e.test.ts + local-checkpoint-native: name: Local checkpoint native needs: changes diff --git a/apps/cli/src/server/local-schema-history.ts b/apps/cli/src/server/local-schema-history.ts index 34988ba39a..dc8c05660e 100644 --- a/apps/cli/src/server/local-schema-history.ts +++ b/apps/cli/src/server/local-schema-history.ts @@ -289,16 +289,16 @@ export const LOCAL_SCHEMA_HISTORY: ReadonlyArray = Obje projectRevision: "ed74788ef292834069e0ea6ee3b22d68fc604fb66cb54d2d551db67ce8d20b3a", }), Object.freeze({ - // v24 adds trace_facets_hourly and its view off trace_list_mv. Nothing is - // rewritten and no row moves; the rollup is not backfilled locally. + // v24 adds trace_facets_hourly and its view off trace_list_mv, backfilled + // from the retained root spans. Nothing existing is rewritten. // // projectRevision is carried forward deliberately: it is a hardcoded // constant that no longer tracks the generator's header, and the identity // this gate compares is the fingerprint/digest pair. version: 24, - fingerprint: "b59c68cd72efa658", - digest: "b59c68cd72efa658d9c5cf602aac7b6cba32947db3d939c03f407c03865be778", - manifestDigest: "a3fc89a7c2ca86dc558f8e5c3f59fdb0bfbbcbe12cb4dce2a9cd8637ae133aef", + fingerprint: "35f37b2e3c43d610", + digest: "35f37b2e3c43d61046bc05f477b4e16c4f0f2ddb2c9100dc5298699014b5a047", + manifestDigest: "09eeccdbd568e6d550b739bb05e074dedb399720902b35577d4eb963c48b0a53", projectRevision: "ed74788ef292834069e0ea6ee3b22d68fc604fb66cb54d2d551db67ce8d20b3a", }), ] as const) diff --git a/apps/cli/src/server/local-store-migrations/steps.ts b/apps/cli/src/server/local-store-migrations/steps.ts index a6b391cb27..ea4dd5ca55 100644 --- a/apps/cli/src/server/local-store-migrations/steps.ts +++ b/apps/cli/src/server/local-store-migrations/steps.ts @@ -1053,17 +1053,22 @@ export const LOCAL_STORE_STEPS: ReadonlyArray = [ ], }, { - // Both objects are new, so the bootstrap's IF NOT EXISTS CREATEs are the whole edge. + // Both objects are new; the backfill rolls up the root spans already retained. id: "local-0023-to-0024-trace-facets-hourly", from: 23, to: 24, description: - "Create trace_facets_hourly and its materialized view so the traces sidebar facets read an hourly rollup instead of scanning trace_list_mv", + "Create trace_facets_hourly and its materialized view, backfilled from trace_list_mv, so the traces sidebar facets read an hourly rollup", clonedBefore: "any DDL runs", + afterBootstrap: [ + backfill( + "INSERT INTO trace_facets_hourly (OrgId, Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError, TraceCount, DurationMin, DurationMax, DurationQuantiles) SELECT OrgId, toStartOfHour(Timestamp) AS Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError, count() AS TraceCount, min(Duration) AS DurationMin, max(Duration) AS DurationMax, quantilesTDigestState(0.5, 0.95)(Duration) AS DurationQuantiles FROM trace_list_mv GROUP BY OrgId, Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError", + ), + ], plan: [ [ "create-trace-facets-hourly", - "Create trace_facets_hourly and trace_facets_hourly_mv via the v24 bootstrap (both new, IF NOT EXISTS)", + "Create trace_facets_hourly and trace_facets_hourly_mv via the v24 bootstrap and backfill the rollup from trace_list_mv", ], ], verifies: "Verify the v24 physical schema and the retained raw telemetry counts", @@ -1072,9 +1077,8 @@ export const LOCAL_STORE_STEPS: ReadonlyArray = [ name: "trace_facets_hourly", classification: "derived", disposition: "rebuild-within-retention-horizon", - guarantee: - "The rollup accrues for root spans ingested after the migration; older ones stay in trace_list_mv until they age out.", - preservationInterval: "from the migration forward", + guarantee: "Rebuilt from every root span trace_list_mv retains; both keep 30 days.", + preservationInterval: "source retention horizon", sourceRetentionDays: 30, targetRetentionDays: 30, }, diff --git a/apps/cli/src/server/schema/local-inserts.json b/apps/cli/src/server/schema/local-inserts.json index 1ee28c0dc9..a7e9b6e96a 100644 --- a/apps/cli/src/server/schema/local-inserts.json +++ b/apps/cli/src/server/schema/local-inserts.json @@ -1,5 +1,5 @@ { - "projectRevision": "eee23295aa263ed5dce623418acbe3cdffac84bbea15570e3ac20b4e1d3b9b50", + "projectRevision": "fdd5816bd9f83325d3bcc1eac0c3d5a872035552bbb1ed9600d7c8464692fe89", "orgPlaceholder": "__ORG__", "datasources": { "traces": { diff --git a/apps/cli/src/server/schema/local-schema-v24.sql b/apps/cli/src/server/schema/local-schema-v24.sql index 260927ae10..ac04a0ba0c 100644 --- a/apps/cli/src/server/schema/local-schema-v24.sql +++ b/apps/cli/src/server/schema/local-schema-v24.sql @@ -876,7 +876,7 @@ CREATE TABLE IF NOT EXISTS trace_facets_hourly ( ENGINE = AggregatingMergeTree PARTITION BY toYYYYMM(Hour) ORDER BY (OrgId, Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError) -TTL toDate(Hour) + INTERVAL 30 DAY; +TTL Hour + INTERVAL 30 DAY; CREATE TABLE IF NOT EXISTS trace_list_mv ( OrgId LowCardinality(String), diff --git a/apps/cli/src/server/schema/local-schema.sql b/apps/cli/src/server/schema/local-schema.sql index 8e1bef6d64..afd6a6e678 100644 --- a/apps/cli/src/server/schema/local-schema.sql +++ b/apps/cli/src/server/schema/local-schema.sql @@ -1,6 +1,6 @@ -- This file is generated by scripts/generate-clickhouse-schema-sql.ts -- Do not edit manually. Run `bun run clickhouse:schema` to regenerate. --- projectRevision: eee23295aa263ed5dce623418acbe3cdffac84bbea15570e3ac20b4e1d3b9b50 +-- projectRevision: fdd5816bd9f83325d3bcc1eac0c3d5a872035552bbb1ed9600d7c8464692fe89 -- localSchemaVersion: 24 CREATE TABLE IF NOT EXISTS ai_crawler_requests ( @@ -876,7 +876,7 @@ CREATE TABLE IF NOT EXISTS trace_facets_hourly ( ENGINE = AggregatingMergeTree PARTITION BY toYYYYMM(Hour) ORDER BY (OrgId, Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError) -TTL toDate(Hour) + INTERVAL 30 DAY; +TTL Hour + INTERVAL 30 DAY; CREATE TABLE IF NOT EXISTS trace_list_mv ( OrgId LowCardinality(String), diff --git a/apps/cli/test/local-store-migrations.test.ts b/apps/cli/test/local-store-migrations.test.ts index a1c7e73509..c3b94f4699 100644 --- a/apps/cli/test/local-store-migrations.test.ts +++ b/apps/cli/test/local-store-migrations.test.ts @@ -93,8 +93,8 @@ const [v10ToV11ProductEventsModule] = localStoreMigrations.filter( describe("current local schema identity", () => { it("matches the generated v24 revision and keeps the issue-297 identity frozen", () => { - expect(SCHEMA_FINGERPRINT).toBe("b59c68cd72efa658") - expect(SCHEMA_DIGEST).toBe("b59c68cd72efa658d9c5cf602aac7b6cba32947db3d939c03f407c03865be778") + expect(SCHEMA_FINGERPRINT).toBe("35f37b2e3c43d610") + expect(SCHEMA_DIGEST).toBe("35f37b2e3c43d61046bc05f477b4e16c4f0f2ddb2c9100dc5298699014b5a047") expect(ISSUE_297_TARGET_SCHEMA_PROJECT_REVISION).toBe( "506bc745f7a7eca202ec905a6403a6815e86413faf0cd3cbbf73881023edce91", ) @@ -170,7 +170,8 @@ describe("current local schema identity", () => { // differs solely through those definitions. v9 removes `error_spans` and // its view; v11 replaces `web_events` with `product_events` and adds // `identity_links`; v17 adds `audit_log`, which local mode creates but - // never writes; v23 adds `ai_crawler_requests` and its view; v24 adds `trace_facets_hourly` and its view. Asserted as an exact set difference rather than a + // never writes; v23 adds `ai_crawler_requests` and its view; v24 adds + // `trace_facets_hourly` and its view. Asserted as an exact set difference rather than a // relaxed check, so a future edge still cannot add or drop an object // unnoticed. expect([...v5Names].filter((name) => !currentNames.has(name))).toEqual([ diff --git a/apps/cli/test/native-local-store-migration.sh b/apps/cli/test/native-local-store-migration.sh index 012221a024..668da85e79 100755 --- a/apps/cli/test/native-local-store-migration.sh +++ b/apps/cli/test/native-local-store-migration.sh @@ -142,7 +142,7 @@ grep -q "local store migrated" "$ROOT/migrate.out" || fail "native migration did # must be bumped in lockstep with LOCAL_SCHEMA_VERSION and the matching # LOCAL_SCHEMA_V.fingerprint in apps/cli/src/server/schema-identity.ts; # leaving it on the previous version is what makes this step fail after a bump. -jq -e '.formatVersion == 2 and .activation == "active" and .schemaVersion == 24 and .schema == "b59c68cd72efa658"' \ +jq -e '.formatVersion == 2 and .activation == "active" and .schemaVersion == 24 and .schema == "35f37b2e3c43d610"' \ "$ROOT/maple-store-version.json" >/dev/null || fail "native migration wrote the wrong active identity" step "reopening promoted store in a fresh server" diff --git a/apps/ingest/src/clickhouse_insert_mappings.rs b/apps/ingest/src/clickhouse_insert_mappings.rs index 2282b2faf0..cc86b5425a 100644 --- a/apps/ingest/src/clickhouse_insert_mappings.rs +++ b/apps/ingest/src/clickhouse_insert_mappings.rs @@ -1,7 +1,7 @@ // This file is generated by scripts/generate-clickhouse-insert-mappings.ts // Do not edit manually. -pub const PROJECT_REVISION: &str = "eee23295aa263ed5dce623418acbe3cdffac84bbea15570e3ac20b4e1d3b9b50"; +pub const PROJECT_REVISION: &str = "fdd5816bd9f83325d3bcc1eac0c3d5a872035552bbb1ed9600d7c8464692fe89"; // Gate for BYO-ClickHouse ingest readiness — the migration version, NOT the // Tinybird-coupled PROJECT_REVISION. Compared against // org_clickhouse_settings.schema_version. See @maple/domain/clickhouse diff --git a/packages/backend/src/services/warehouse/trace-facets-hourly-materialization.clickhouse.e2e.test.ts b/packages/backend/src/services/warehouse/trace-facets-hourly-materialization.clickhouse.e2e.test.ts index 604e6e4a7a..f9a16e3c43 100644 --- a/packages/backend/src/services/warehouse/trace-facets-hourly-materialization.clickhouse.e2e.test.ts +++ b/packages/backend/src/services/warehouse/trace-facets-hourly-materialization.clickhouse.e2e.test.ts @@ -1,11 +1,10 @@ // SAFETY-FILE: JSON in this test is emitted by ClickHouse before its fields are asserted. // `trace_facets_hourly` is cascaded off `trace_list_mv`: it fires on the insert -// blocks another view writes, not on `traces`. That only works if ClickHouse -// actually chains the two views, and a rollup nobody fills reads as "no data" -// rather than as an error. So this suite proves rows: it inserts spans into -// `traces` on a database built from the real migration chain and asserts the -// rollup agrees with `trace_list_mv` group for group, then that migration 0034's -// backfill reproduces the same rollup from `trace_list_mv` alone. +// blocks another view writes, not on `traces`, and a rollup nobody fills reads +// as "no data" rather than as an error. So this proves rows: spans inserted +// into `traces` on a database built from the real migration chain must roll up +// to exactly what `trace_list_mv` holds, and migration 0034's backfill must +// rebuild the same rollup from `trace_list_mv` alone. import { afterAll, assert, beforeAll, describe, it } from "@effect/vitest" import { migrations, renderStatementFull } from "@maple/domain/clickhouse" @@ -21,104 +20,46 @@ const ORG_ID = "org_trace_facets_e2e" // Anchored to now: `traces` carries a 30-day TTL enforced at insert. const HOUR_MS = 3_600_000 -const BASE_MS = Math.floor((Date.now() - 3 * HOUR_MS) / HOUR_MS) * HOUR_MS +const BASE_MS = Math.floor((Date.now() - 4 * HOUR_MS) / HOUR_MS) * HOUR_MS -const chDateTime = (epochMs: number): string => new Date(epochMs).toISOString().replace("T", " ").slice(0, 23) -const quote = (value: string): string => `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'` -const chMap = (attrs: Readonly>): string => - `map(${Object.entries(attrs) - .flatMap(([key, value]) => [quote(key), quote(value)]) - .join(", ")})` +const chDateTime = (epochMs: number): string => new Date(epochMs).toISOString().replace("T", " ").slice(0, 19) interface SeedSpan { readonly traceId: string - readonly parentSpanId?: string - readonly name: string readonly ms: number readonly service: string - readonly status?: string readonly durationNs: number - readonly attrs?: Readonly> - readonly resource?: Readonly> + readonly parentSpanId?: string + readonly status?: string + readonly env?: string } -const PROD = { "deployment.environment.name": "production", "service.namespace": "core" } +const root = (traceId: string, ms: number, service: string, durationNs: number, extra = {}): SeedSpan => ({ + traceId, + ms, + service, + durationNs, + env: "production", + ...extra, +}) +// Two insert blocks share the (hour, api, production) group, so the rollup holds +// two partial rows for it until a merge and reads must sum them. const BATCH_1: ReadonlyArray = [ - // Old and new HTTP semconv for the same request: one group after coalescing. - { - traceId: "t1", - name: "http.server GET", - ms: BASE_MS + 60_000, - service: "api", - durationNs: 5_000_000, - attrs: { "http.method": "GET", "http.route": "/users", "http.status_code": "200" }, - resource: PROD, - }, - { - traceId: "t2", - name: "http.server GET", - ms: BASE_MS + 120_000, - service: "api", - durationNs: 9_000_000, - attrs: { "http.request.method": "GET", "http.route": "/users", "http.response.status_code": "200" }, - resource: PROD, - }, - // A 5xx: HasError without an Error status. - { - traceId: "t3", - name: "POST", - ms: BASE_MS + 180_000, - service: "api", - durationNs: 50_000_000, - attrs: { "http.request.method": "POST", "url.path": "/orders", "http.response.status_code": "503" }, - resource: PROD, - }, - { - traceId: "t4", - name: "cron.tick", - ms: BASE_MS + HOUR_MS + 60_000, - service: "worker", - status: "Error", - durationNs: 1_000_000, - resource: { "deployment.environment": "staging" }, - }, + root("t1", BASE_MS + 60_000, "api", 5_000_000), + root("t2", BASE_MS + 120_000, "api", 9_000_000), + root("t3", BASE_MS + 180_000, "worker", 1_000_000, { status: "Error", env: "staging" }), + root("t4", BASE_MS + HOUR_MS + 60_000, "api", 2_000_000), // Not a root span: never reaches trace_list_mv, so never the rollup. - { - traceId: "t1", - parentSpanId: "root-t1", - name: "db.query", - ms: BASE_MS + 61_000, - service: "api", - durationNs: 2_000_000, - }, -] - -// A second insert block into an hour the first one already wrote: the rollup -// holds two partial rows for that group until a merge, and reads must sum them. -const BATCH_2: ReadonlyArray = [ - { - traceId: "t5", - name: "http.server GET", - ms: BASE_MS + 240_000, - service: "api", - durationNs: 1_000_000, - attrs: { "http.method": "GET", "http.route": "/users", "http.status_code": "200" }, - resource: PROD, - }, + { traceId: "t1", ms: BASE_MS + 61_000, service: "api", durationNs: 1_000, parentSpanId: "root-t1" }, ] - -const traceFacetsHourlyBackfill = migrations - .find((migration) => migration.version === 34)! - .statements.find((statement) => typeof statement !== "string")! - -const ROOT_SPAN_COUNT = [...BATCH_1, ...BATCH_2].filter((span) => !span.parentSpanId).length +const BATCH_2: ReadonlyArray = [root("t5", BASE_MS + 240_000, "api", 3_000_000)] const insert = async (spans: ReadonlyArray): Promise => { const rows = spans .map( (span, index) => - `(${quote(ORG_ID)}, ${quote(chDateTime(span.ms))}, ${quote(span.traceId)}, ${quote(`span-${span.traceId}-${index}`)}, ${quote(span.parentSpanId ?? "")}, ${quote(span.name)}, 'Server', ${quote(span.service)}, ${span.durationNs}, ${quote(span.status ?? "Ok")}, '', 1, ${chMap(span.attrs ?? {})}, ${chMap(span.resource ?? {})})`, + `('${ORG_ID}', '${chDateTime(span.ms)}', '${span.traceId}', 'span-${span.traceId}-${index}', '${span.parentSpanId ?? ""}', 'op', 'Server', '${span.service}', ${span.durationNs}, '${span.status ?? "Ok"}', '', 1, map(), map('deployment.environment.name', '${span.env ?? ""}'))`, ) .join("\n,") await clickhouseExec( @@ -144,7 +85,7 @@ const fromTraceList = () => `SELECT toString(toStartOfHour(Timestamp)) AS Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError, count() AS traces, min(Duration) AS durationMin, max(Duration) AS durationMax - FROM trace_list_mv WHERE OrgId = ${quote(ORG_ID)} + FROM trace_list_mv WHERE OrgId = '${ORG_ID}' GROUP BY ${DIMENSIONS} ORDER BY ${DIMENSIONS}`, ) @@ -153,7 +94,7 @@ const fromRollup = () => `SELECT toString(Hour) AS Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError, sum(TraceCount) AS traces, min(DurationMin) AS durationMin, max(DurationMax) AS durationMax - FROM trace_facets_hourly WHERE OrgId = ${quote(ORG_ID)} + FROM trace_facets_hourly WHERE OrgId = '${ORG_ID}' GROUP BY ${DIMENSIONS} ORDER BY ${DIMENSIONS}`, ) @@ -169,36 +110,19 @@ describe.skipIf(!clickhouseE2eEnabled)("trace_facets_hourly materialization", () await clickhouseExec(`DROP DATABASE IF EXISTS ${database}`) }, 30_000) - it("rolls up every root span trace_list_mv holds, group for group", async () => { + it("rolls up every root span of trace_list_mv, live and from migration 0034's backfill", async () => { const expected = await fromTraceList() - const actual = await fromRollup() - - assert.deepStrictEqual(actual, expected) assert.strictEqual( - actual.reduce((total, row) => total + Number(row.traces), 0), - ROOT_SPAN_COUNT, + expected.reduce((total, row) => total + Number(row.traces), 0), + 5, ) - // The coalesced semconv spellings and the rewritten span name landed in one group. - assert.deepInclude(actual, { - Hour: chDateTime(BASE_MS).slice(0, 19), - ServiceName: "api", - SpanName: "GET /users", - HttpMethod: "GET", - HttpStatusCode: "200", - DeploymentEnv: "production", - ServiceNamespace: "core", - HasError: 0, - traces: 3, - durationMin: 1_000_000, - durationMax: 9_000_000, - }) - }) + assert.deepStrictEqual(await fromRollup(), expected) - it("reproduces the same rollup from migration 0034's backfill", async () => { - const expected = await fromRollup() + const backfill = migrations + .find((migration) => migration.version === 34)! + .statements.find((statement) => typeof statement !== "string")! await clickhouseExec("TRUNCATE TABLE trace_facets_hourly", database) - await clickhouseExec(renderStatementFull(traceFacetsHourlyBackfill, database), database) - + await clickhouseExec(renderStatementFull(backfill, database), database) assert.deepStrictEqual(await fromRollup(), expected) }) }) diff --git a/packages/backend/src/services/warehouse/warehouse-catalog.ts b/packages/backend/src/services/warehouse/warehouse-catalog.ts index cd42e48ea4..28a100888b 100644 --- a/packages/backend/src/services/warehouse/warehouse-catalog.ts +++ b/packages/backend/src/services/warehouse/warehouse-catalog.ts @@ -45,11 +45,6 @@ const TABLE_NOTES: Record> = { "Scanners borrow crawler user agents and mostly get 404s: count pages actually read with `HttpStatus < 400` (0 means the span carried no status).", "Sorting key: `(OrgId, Timestamp, TraceId)`; filled forward by its MV from migration 0033, never backfilled.", ], - trace_facets_hourly: [ - "Hourly root-span (one per trace) counts by `ServiceName`, `SpanName`, `HttpMethod`, `HttpStatusCode`, `DeploymentEnv`, `ServiceNamespace` and `HasError`. ALWAYS prefer this over `trace_list_mv` for trace counts or top-N of those dimensions over day-plus windows.", - "Time column is `Hour`; count with `sum(TraceCount)`. `DurationMin`/`DurationMax` are NANOSECONDS (`min`/`max` them); percentiles via `quantilesTDigestMerge(0.5, 0.95)(DurationQuantiles)`.", - "Sorting key: `(OrgId, Hour, ServiceName, SpanName, …)`.", - ], service_overview_spans: [ "Pre-materialized projection of entry-point spans only (Server/Consumer kinds + root spans). Use for per-service request count, error rate, p50/p95/p99 latency.", "`Duration` is NANOSECONDS — divide by 1e6 for ms.", diff --git a/packages/domain/src/clickhouse/migrations/0034_trace_facets_hourly.ts b/packages/domain/src/clickhouse/migrations/0034_trace_facets_hourly.ts index d402e590f7..81f04c004e 100644 --- a/packages/domain/src/clickhouse/migrations/0034_trace_facets_hourly.ts +++ b/packages/domain/src/clickhouse/migrations/0034_trace_facets_hourly.ts @@ -18,10 +18,8 @@ const TRACE_FACETS_GROUP_BY = "OrgId, Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError" /** - * Backfill for the 30 days `trace_list_mv` retains, which is the rollup's own - * retention, so the rollup is complete from the moment the view attaches. The - * backfill and the view share one projection and one source; day-aligned chunks - * are coarser than the hourly groups, so no group straddles a chunk. + * Backfills the 30 days `trace_list_mv` retains (the rollup's own retention). + * Shares the view's projection; day chunks never split an hourly group. */ export const traceFacetsHourlyBackfill: BackfillSpec = { kind: "backfill", @@ -49,17 +47,8 @@ export const traceFacetsHourlyBackfill: BackfillSpec = { /** * Migration 0034: `trace_facets_hourly`, the hourly rollup behind the traces - * sidebar facets and duration stats. - * - * Those queries scanned every root span in the window on `trace_list_mv`, and - * past about a day they exceeded the 5s discovery budget. The rollup is - * cascaded off `trace_list_mv`, so its values are the ones the trace list - * filters on. - * - * DROP VIEW → CREATE TABLE → TRUNCATE → backfill → CREATE VIEW, as in 0015: the - * truncate makes a re-apply converge instead of doubling, and it is safe only - * because the view is detached first. `requiredForIngest: false`: nothing - * writes the table directly. + * sidebar facets. Statement order as in 0015: the truncate makes a re-apply + * converge, and is safe only because the view is detached first. * * The CREATE statements are the verbatim DDL the schema emitter produced at v34. * Frozen history: never re-derive it from a later snapshot. @@ -71,7 +60,7 @@ export const migration_0034_trace_facets_hourly = { requiredForIngest: false, statements: [ "DROP VIEW IF EXISTS trace_facets_hourly_mv", - "CREATE TABLE IF NOT EXISTS trace_facets_hourly (\n OrgId LowCardinality(String),\n Hour DateTime,\n ServiceName LowCardinality(String),\n SpanName String,\n HttpMethod LowCardinality(String),\n HttpStatusCode LowCardinality(String),\n DeploymentEnv LowCardinality(String),\n ServiceNamespace LowCardinality(String),\n HasError UInt8,\n TraceCount SimpleAggregateFunction(sum, UInt64),\n DurationMin SimpleAggregateFunction(min, UInt64),\n DurationMax SimpleAggregateFunction(max, UInt64),\n DurationQuantiles AggregateFunction(quantilesTDigest(0.5, 0.95), UInt64)\n)\nENGINE = AggregatingMergeTree\nPARTITION BY toYYYYMM(Hour)\nORDER BY (OrgId, Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError)\nTTL toDate(Hour) + INTERVAL 30 DAY", + "CREATE TABLE IF NOT EXISTS trace_facets_hourly (\n OrgId LowCardinality(String),\n Hour DateTime,\n ServiceName LowCardinality(String),\n SpanName String,\n HttpMethod LowCardinality(String),\n HttpStatusCode LowCardinality(String),\n DeploymentEnv LowCardinality(String),\n ServiceNamespace LowCardinality(String),\n HasError UInt8,\n TraceCount SimpleAggregateFunction(sum, UInt64),\n DurationMin SimpleAggregateFunction(min, UInt64),\n DurationMax SimpleAggregateFunction(max, UInt64),\n DurationQuantiles AggregateFunction(quantilesTDigest(0.5, 0.95), UInt64)\n)\nENGINE = AggregatingMergeTree\nPARTITION BY toYYYYMM(Hour)\nORDER BY (OrgId, Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError)\nTTL Hour + INTERVAL 30 DAY", "TRUNCATE TABLE IF EXISTS trace_facets_hourly", traceFacetsHourlyBackfill, `CREATE MATERIALIZED VIEW IF NOT EXISTS trace_facets_hourly_mv TO trace_facets_hourly AS\nSELECT\n ${TRACE_FACETS_PROJECTION_SQL}\n FROM trace_list_mv\n GROUP BY ${TRACE_FACETS_GROUP_BY}`, diff --git a/packages/domain/src/clickhouse/migrations/index.test.ts b/packages/domain/src/clickhouse/migrations/index.test.ts index fc9bb0468b..5850b21fdc 100644 --- a/packages/domain/src/clickhouse/migrations/index.test.ts +++ b/packages/domain/src/clickhouse/migrations/index.test.ts @@ -834,21 +834,12 @@ describe("migration 0031 — ai_trace_index list columns", () => { }) describe("migration 0034: trace_facets_hourly", () => { - it("backfills from trace_list_mv with the view detached, then attaches it", () => { + it("backfills with the view detached, then attaches it, in the emitter's DDL", () => { const [drop, table, truncate, backfill, view, ...rest] = migration_0034_trace_facets_hourly.statements expect(rest).toEqual([]) expect(drop).toBe("DROP VIEW IF EXISTS trace_facets_hourly_mv") expect(truncate).toBe("TRUNCATE TABLE IF EXISTS trace_facets_hourly") expect(backfill).toBe(traceFacetsHourlyBackfill) - expect(traceFacetsHourlyBackfill.from).toBe("trace_list_mv") - expect(traceFacetsHourlyBackfill.target).toBe("trace_facets_hourly") - // The view and the backfill must write the same groups, or backfilled - // hours and live hours disagree with no dedup to catch it. - expect(view).toContain( - `SELECT\n ${traceFacetsHourlyBackfill.select}\n FROM trace_list_mv`, - ) - expect(view).toContain(`GROUP BY ${traceFacetsHourlyBackfill.groupBy}`) - // Frozen DDL: identical to what the emitter produces for the current schema. expect(latestSnapshotStatements).toContain(table) expect(latestSnapshotStatements).toContain(view) }) diff --git a/packages/domain/src/generated/clickhouse-schema.ts b/packages/domain/src/generated/clickhouse-schema.ts index 12e147f18e..ac28b2f439 100644 --- a/packages/domain/src/generated/clickhouse-schema.ts +++ b/packages/domain/src/generated/clickhouse-schema.ts @@ -1,7 +1,7 @@ // This file is generated by scripts/generate-clickhouse-schema.ts // Do not edit manually. -export const projectRevision = "eee23295aa263ed5dce623418acbe3cdffac84bbea15570e3ac20b4e1d3b9b50" as const +export const projectRevision = "fdd5816bd9f83325d3bcc1eac0c3d5a872035552bbb1ed9600d7c8464692fe89" as const export const latestSnapshotStatements: ReadonlyArray = [ "CREATE TABLE IF NOT EXISTS ai_crawler_requests (\n OrgId LowCardinality(String),\n Timestamp DateTime64(9),\n TraceId String,\n ServiceName LowCardinality(String),\n Crawler LowCardinality(String),\n Host LowCardinality(String),\n Path String,\n HttpStatus UInt16\n)\nENGINE = MergeTree\nPARTITION BY toDate(Timestamp)\nORDER BY (OrgId, Timestamp, TraceId)\nTTL toDate(Timestamp) + INTERVAL 30 DAY", @@ -42,7 +42,7 @@ export const latestSnapshotStatements: ReadonlyArray = [ "CREATE TABLE IF NOT EXISTS session_replays (\n OrgId LowCardinality(String),\n SessionId String,\n StartTime DateTime64(9),\n EndTime Nullable(DateTime64(9)),\n DurationMs Nullable(UInt32),\n Status LowCardinality(String),\n UserId String,\n UrlInitial String,\n UserAgent String,\n BrowserName LowCardinality(String),\n OsName LowCardinality(String),\n DeviceType LowCardinality(String),\n Country LowCardinality(String) DEFAULT '',\n ServiceName LowCardinality(String),\n PageViews UInt32 DEFAULT 0,\n ClickCount UInt32 DEFAULT 0,\n ErrorCount UInt32 DEFAULT 0,\n TraceIds Array(String) DEFAULT [],\n ResourceAttributes Map(LowCardinality(String), String),\n Version UInt32,\n VisitorId String DEFAULT '',\n VisitorIsNew UInt8 DEFAULT 0,\n UserEmail String DEFAULT '',\n UserName String DEFAULT '',\n GroupId String DEFAULT '',\n GroupName String DEFAULT '',\n UserTraits Map(String, String) DEFAULT map(),\n Referrer String DEFAULT '',\n ReferrerHost LowCardinality(String) DEFAULT '',\n UtmSource LowCardinality(String) DEFAULT '',\n UtmMedium LowCardinality(String) DEFAULT '',\n UtmCampaign LowCardinality(String) DEFAULT '',\n UtmTerm String DEFAULT '',\n UtmContent String DEFAULT '',\n Host LowCardinality(String) DEFAULT '',\n EntryPath String DEFAULT '',\n ExitPath String DEFAULT '',\n Language LowCardinality(String) DEFAULT '',\n LastActivityAt Nullable(DateTime64(9))\n)\nENGINE = ReplacingMergeTree\nPARTITION BY toDate(StartTime)\nORDER BY (OrgId, SessionId)\nTTL toDate(StartTime) + INTERVAL 30 DAY", "CREATE TABLE IF NOT EXISTS span_metrics_calls_hourly (\n OrgId LowCardinality(String),\n Hour DateTime,\n ServiceName LowCardinality(String),\n MetricName LowCardinality(String),\n SpanKind LowCardinality(String),\n AttrFingerprint UInt64,\n ResourceFingerprint UInt64,\n StartTimeUnix DateTime64(9),\n LastValue AggregateFunction(argMax, Float64, DateTime64(9))\n)\nENGINE = AggregatingMergeTree\nPARTITION BY toDate(Hour)\nORDER BY (OrgId, Hour, ServiceName, MetricName, SpanKind, AttrFingerprint, ResourceFingerprint, StartTimeUnix)\nTTL toDate(Hour) + INTERVAL 90 DAY", "CREATE TABLE IF NOT EXISTS trace_detail_spans (\n OrgId LowCardinality(String),\n Timestamp DateTime64(9),\n TraceId String,\n SpanId String,\n ParentSpanId String,\n SpanName LowCardinality(String),\n SpanKind LowCardinality(String),\n ServiceName LowCardinality(String),\n Duration UInt64 DEFAULT 0,\n StatusCode LowCardinality(String),\n StatusMessage String,\n SpanAttributes Map(LowCardinality(String), String),\n ResourceAttributes Map(LowCardinality(String), String)\n)\nENGINE = MergeTree\nPARTITION BY toDate(Timestamp)\nORDER BY (OrgId, TraceId, SpanId)\nTTL toDate(Timestamp) + INTERVAL 30 DAY", - "CREATE TABLE IF NOT EXISTS trace_facets_hourly (\n OrgId LowCardinality(String),\n Hour DateTime,\n ServiceName LowCardinality(String),\n SpanName String,\n HttpMethod LowCardinality(String),\n HttpStatusCode LowCardinality(String),\n DeploymentEnv LowCardinality(String),\n ServiceNamespace LowCardinality(String),\n HasError UInt8,\n TraceCount SimpleAggregateFunction(sum, UInt64),\n DurationMin SimpleAggregateFunction(min, UInt64),\n DurationMax SimpleAggregateFunction(max, UInt64),\n DurationQuantiles AggregateFunction(quantilesTDigest(0.5, 0.95), UInt64)\n)\nENGINE = AggregatingMergeTree\nPARTITION BY toYYYYMM(Hour)\nORDER BY (OrgId, Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError)\nTTL toDate(Hour) + INTERVAL 30 DAY", + "CREATE TABLE IF NOT EXISTS trace_facets_hourly (\n OrgId LowCardinality(String),\n Hour DateTime,\n ServiceName LowCardinality(String),\n SpanName String,\n HttpMethod LowCardinality(String),\n HttpStatusCode LowCardinality(String),\n DeploymentEnv LowCardinality(String),\n ServiceNamespace LowCardinality(String),\n HasError UInt8,\n TraceCount SimpleAggregateFunction(sum, UInt64),\n DurationMin SimpleAggregateFunction(min, UInt64),\n DurationMax SimpleAggregateFunction(max, UInt64),\n DurationQuantiles AggregateFunction(quantilesTDigest(0.5, 0.95), UInt64)\n)\nENGINE = AggregatingMergeTree\nPARTITION BY toYYYYMM(Hour)\nORDER BY (OrgId, Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError)\nTTL Hour + INTERVAL 30 DAY", "CREATE TABLE IF NOT EXISTS trace_list_mv (\n OrgId LowCardinality(String),\n TraceId String,\n Timestamp DateTime,\n ServiceName LowCardinality(String),\n SpanName String,\n SpanKind LowCardinality(String),\n Duration UInt64,\n StatusCode LowCardinality(String),\n HttpMethod LowCardinality(String),\n HttpRoute String,\n HttpStatusCode LowCardinality(String),\n DeploymentEnv LowCardinality(String),\n HasError UInt8,\n TraceState String,\n ServiceNamespace LowCardinality(String),\n INDEX idx_service_namespace ServiceNamespace TYPE set(1000) GRANULARITY 4\n)\nENGINE = MergeTree\nPARTITION BY toDate(Timestamp)\nORDER BY (OrgId, Timestamp, TraceId)\nTTL Timestamp + INTERVAL 30 DAY", "CREATE TABLE IF NOT EXISTS traces (\n OrgId LowCardinality(String),\n Timestamp DateTime64(9),\n TraceId String,\n SpanId String,\n ParentSpanId String,\n TraceState String,\n SpanName LowCardinality(String),\n SpanKind LowCardinality(String),\n ServiceName LowCardinality(String),\n ResourceSchemaUrl String,\n ResourceAttributes Map(LowCardinality(String), String),\n ScopeSchemaUrl String,\n ScopeName String,\n ScopeVersion String,\n ScopeAttributes Map(LowCardinality(String), String),\n Duration UInt64 DEFAULT 0,\n StatusCode LowCardinality(String),\n StatusMessage String,\n SpanAttributes Map(LowCardinality(String), String),\n EventsTimestamp Array(DateTime64(9)),\n EventsName Array(LowCardinality(String)),\n EventsAttributes Array(Map(LowCardinality(String), String)),\n LinksTraceId Array(String),\n LinksSpanId Array(String),\n LinksTraceState Array(String),\n LinksAttributes Array(Map(LowCardinality(String), String)),\n SampleRate Float64 DEFAULT multiIf(SpanAttributes['SampleRate'] != '' AND toFloat64OrZero(SpanAttributes['SampleRate']) >= 1.0, toFloat64OrZero(SpanAttributes['SampleRate']), match(TraceState, 'th:[0-9a-f]+'), 1.0 / greatest(1.0 - reinterpretAsUInt64(reverse(unhex(rightPad(extract(TraceState, 'th:([0-9a-f]+)'), 16, '0')))) / pow(2.0, 64), 0.0001), 1.0),\n IsEntryPoint UInt8 DEFAULT if(SpanKind IN ('Server', 'Consumer') OR ParentSpanId = '', 1, 0),\n ResourceAttributeItems Array(String) DEFAULT arrayMap((k, v) -> concat(k, char(31), v), mapKeys(ResourceAttributes), mapValues(ResourceAttributes)),\n ScopeAttributeItems Array(String) DEFAULT arrayMap((k, v) -> concat(k, char(31), v), mapKeys(ScopeAttributes), mapValues(ScopeAttributes)),\n SpanAttributeItems Array(String) DEFAULT arrayMap((k, v) -> concat(k, char(31), v), mapKeys(SpanAttributes), mapValues(SpanAttributes)),\n INDEX idx_trace_id TraceId TYPE bloom_filter(0.01) GRANULARITY 1,\n INDEX idx_span_attr_keys mapKeys(SpanAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,\n INDEX idx_span_attr_vals mapValues(SpanAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,\n INDEX idx_resource_attr_keys mapKeys(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,\n INDEX idx_resource_attr_vals mapValues(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,\n INDEX idx_scope_attr_keys mapKeys(ScopeAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,\n INDEX idx_scope_attr_vals mapValues(ScopeAttributes) TYPE bloom_filter(0.01) GRANULARITY 1\n)\nENGINE = MergeTree\nPARTITION BY toDate(Timestamp)\nORDER BY (OrgId, ServiceName, SpanName, toDateTime(Timestamp))\nTTL toDate(Timestamp) + INTERVAL 30 DAY", "CREATE TABLE IF NOT EXISTS traces_aggregates_hourly (\n OrgId LowCardinality(String),\n Hour DateTime,\n ServiceName LowCardinality(String),\n SpanName LowCardinality(String),\n SpanKind LowCardinality(String),\n StatusCode LowCardinality(String),\n IsEntryPoint UInt8,\n DeploymentEnv LowCardinality(String),\n WeightedCount SimpleAggregateFunction(sum, Float64),\n WeightedDurationSum SimpleAggregateFunction(sum, Float64),\n WeightedErrorCount SimpleAggregateFunction(sum, Float64),\n DurationQuantiles AggregateFunction(quantilesTDigestWeighted(0.5, 0.95, 0.99), UInt64, UInt32),\n DurationMin SimpleAggregateFunction(min, UInt64),\n DurationMax SimpleAggregateFunction(max, UInt64)\n)\nENGINE = AggregatingMergeTree\nPARTITION BY toDate(Hour)\nORDER BY (OrgId, Hour, ServiceName, SpanName, SpanKind, StatusCode, IsEntryPoint, DeploymentEnv)\nTTL toDate(Hour) + INTERVAL 365 DAY", diff --git a/packages/domain/src/generated/tinybird-project-manifest.ts b/packages/domain/src/generated/tinybird-project-manifest.ts index 287d020512..e5f778af07 100644 --- a/packages/domain/src/generated/tinybird-project-manifest.ts +++ b/packages/domain/src/generated/tinybird-project-manifest.ts @@ -1,7 +1,7 @@ // This file is generated by scripts/generate-tinybird-project-manifest.ts // Do not edit manually. -export const projectRevision = "eee23295aa263ed5dce623418acbe3cdffac84bbea15570e3ac20b4e1d3b9b50" as const +export const projectRevision = "fdd5816bd9f83325d3bcc1eac0c3d5a872035552bbb1ed9600d7c8464692fe89" as const export const datasources = [ { @@ -197,7 +197,7 @@ export const datasources = [ { name: "trace_facets_hourly", content: - 'DESCRIPTION >\n Hourly root-span counts and duration state per service, span name, HTTP method/status, environment, namespace and error flag. Traces sidebar facets. Populated by materialized view from trace_list_mv.\n\nSCHEMA >\n OrgId LowCardinality(String),\n Hour DateTime,\n ServiceName LowCardinality(String),\n SpanName String,\n HttpMethod LowCardinality(String),\n HttpStatusCode LowCardinality(String),\n DeploymentEnv LowCardinality(String),\n ServiceNamespace LowCardinality(String),\n HasError UInt8,\n TraceCount SimpleAggregateFunction(sum, UInt64),\n DurationMin SimpleAggregateFunction(min, UInt64),\n DurationMax SimpleAggregateFunction(max, UInt64),\n DurationQuantiles AggregateFunction(quantilesTDigest(0.5, 0.95), UInt64)\n\nENGINE "AggregatingMergeTree"\nENGINE_PARTITION_KEY "toYYYYMM(Hour)"\nENGINE_SORTING_KEY "OrgId, Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError"\nENGINE_TTL "toDate(Hour) + INTERVAL 30 DAY"', + 'DESCRIPTION >\n Hourly root-span counts and duration state per service, span name, HTTP method/status, environment, namespace and error flag. Traces sidebar facets. Populated by materialized view from trace_list_mv.\n\nSCHEMA >\n OrgId LowCardinality(String),\n Hour DateTime,\n ServiceName LowCardinality(String),\n SpanName String,\n HttpMethod LowCardinality(String),\n HttpStatusCode LowCardinality(String),\n DeploymentEnv LowCardinality(String),\n ServiceNamespace LowCardinality(String),\n HasError UInt8,\n TraceCount SimpleAggregateFunction(sum, UInt64),\n DurationMin SimpleAggregateFunction(min, UInt64),\n DurationMax SimpleAggregateFunction(max, UInt64),\n DurationQuantiles AggregateFunction(quantilesTDigest(0.5, 0.95), UInt64)\n\nENGINE "AggregatingMergeTree"\nENGINE_PARTITION_KEY "toYYYYMM(Hour)"\nENGINE_SORTING_KEY "OrgId, Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError"\nENGINE_TTL "Hour + INTERVAL 30 DAY"', }, { name: "trace_list_mv", diff --git a/packages/domain/src/tinybird/datasources.ts b/packages/domain/src/tinybird/datasources.ts index 04dc54e029..29b7952314 100644 --- a/packages/domain/src/tinybird/datasources.ts +++ b/packages/domain/src/tinybird/datasources.ts @@ -1046,19 +1046,10 @@ export const traceListMv = defineDatasource("trace_list_mv", { export type TraceListMvRow = InferRow /** - * Hourly rollup of `trace_list_mv` over the traces sidebar's facet dimensions, - * for `tracesFacetsQuery` and `tracesDurationStatsQuery`. - * - * Those queries count root spans per facet value over the page's window. On - * `trace_list_mv` that is a scan of every root span in the window (~14M/day - * for a busy org), and past about a day it exceeds the 5s discovery budget. - * The dimensions collapse to a few hundred combinations per org-hour, so the - * same answer is a read of a few thousand rows here. - * - * Every dimension is in the sorting key: on an AggregatingMergeTree a non-key - * column merges rows together and sums across the values you meant to keep - * apart. Cascaded off `trace_list_mv`, so the span-name rewrite and the HTTP - * semconv coalescing are the ones the trace list already filters on. + * Hourly rollup of `trace_list_mv` for the traces sidebar facets and duration + * stats. Scanning `trace_list_mv` itself (~14M root spans/day for a busy org) + * exceeds the 5s discovery budget past about a day; the facet dimensions + * collapse to a few hundred rows per org-hour. */ export const traceFacetsHourly = defineDatasource("trace_facets_hourly", { description: @@ -1092,7 +1083,7 @@ export const traceFacetsHourly = defineDatasource("trace_facets_hourly", { "ServiceNamespace", "HasError", ], - ttl: "toDate(Hour) + INTERVAL 30 DAY", + ttl: "Hour + INTERVAL 30 DAY", }), }) diff --git a/packages/domain/src/tinybird/materializations.ts b/packages/domain/src/tinybird/materializations.ts index d4a95a8f0c..b77078efe3 100644 --- a/packages/domain/src/tinybird/materializations.ts +++ b/packages/domain/src/tinybird/materializations.ts @@ -1173,12 +1173,7 @@ export const traceListMvMv = defineMaterializedView("trace_list_mv_mv", { ], }) -/** - * Cascading hourly rollup of `trace_list_mv` for the traces sidebar facets. It - * consumes the root-span insert stream `trace_list_mv_mv` already produced, so - * its dimensions are the trace list's exact values rather than a fourth copy of - * the span-name rewrite. - */ +/** Cascaded off `trace_list_mv`, so it groups by the values the trace list filters on. */ export const traceFacetsHourlyMv = defineMaterializedView("trace_facets_hourly_mv", { description: "Rolls trace_list_mv up hourly by the traces sidebar facet dimensions, with root-span counts and duration state.", From 8af8ec34444ffd5d1cd0d4e579ddeb91339e962e Mon Sep 17 00:00:00 2001 From: JeremyFunk Date: Mon, 28 Sep 2026 10:31:17 +0200 Subject: [PATCH 4/7] refactor(traces): one splice path for sidebar facets, raw fallback without 0034 - Facets and duration stats build one union: the trace_list_mv tier, plus the trace_facets_hourly interior when the rollup can answer. The trace_list_mv-only route is the same union with one tier, instead of a second copy of every branch. - rawOnly reads trace_list_mv only. The runtime retries with it when the cluster lacks trace_facets_hourly (a BYO cluster that has not applied 0034), the same way it handles service_overview_minutely; the pipe surface (MCP, CLI) has no retry, so it keeps the raw read. - The rollup SQL enters the catalog through builder fixtures, since the pipe fixtures now compile the raw route. - e2e parity now seeds rows on both raw edges and a max only the hourly tier holds, and adds an hour-aligned window whose raw tier is empty. - Trim SQL-text asserts the splice gate and rollup-splice tests already cover. --- docs/warehouse-rollups.md | 2 +- .../warehouse/QueryEngineService.test.ts | 45 +- ...rly-materialization.clickhouse.e2e.test.ts | 45 +- .../src/__sql_baseline__/catalog.sql | 1041 +++++++---------- .../query-engine/src/benchmark/builders.ts | 14 + .../src/benchmark/catalog.test.ts | 2 - .../query-engine/src/benchmark/catalog.ts | 23 - packages/query-engine/src/ch/ch.test.ts | 2 - packages/query-engine/src/ch/pipe-dispatch.ts | 4 + .../src/ch/queries/errors.test.ts | 25 +- .../query-engine/src/ch/queries/errors.ts | 176 ++- packages/query-engine/src/ch/tables.ts | 3 - .../query-engine/src/runtime/query-engine.ts | 73 +- 13 files changed, 650 insertions(+), 805 deletions(-) diff --git a/docs/warehouse-rollups.md b/docs/warehouse-rollups.md index 3f202663a5..db12e432e0 100644 --- a/docs/warehouse-rollups.md +++ b/docs/warehouse-rollups.md @@ -20,7 +20,7 @@ DDL snapshot, `local-schema.sql` for the embedded chDB engine, and the Rust inse **A query must read the coarsest tier that can answer it.** The routing guards (`canUseAnnualServiceOverview`, `canUseTracesAggregatesMv`, `canUseServiceOverviewMv`, -`canUseLogsAggregatesHourly`) exist to enforce that, and each one names the tier it unlocks. +`canUseLogsAggregatesHourly`, `canUseTraceFacetsRollup`) exist to enforce that, and each one names the tier it unlocks. Rollup routes union a **raw edge** with a **rollup interior**: the rollup answers whole buckets, and the raw table covers the partial buckets at each end of the window. Getting the diff --git a/packages/backend/src/services/warehouse/QueryEngineService.test.ts b/packages/backend/src/services/warehouse/QueryEngineService.test.ts index 9b884ddfbc..e73ee774c2 100644 --- a/packages/backend/src/services/warehouse/QueryEngineService.test.ts +++ b/packages/backend/src/services/warehouse/QueryEngineService.test.ts @@ -4,7 +4,7 @@ import { TestClock } from "effect/testing" import { Deferred, Effect, Exit, Fiber, Option, Schema } from "effect" import { strict as nodeAssert } from "node:assert" import { MetricName, OrgId, ServiceName, UserId } from "@maple/domain" -import { RawSqlValidationError, WarehouseUpstreamError } from "@maple/domain/http" +import { RawSqlValidationError, WarehouseConfigError, WarehouseUpstreamError } from "@maple/domain/http" import { baselineWarehouseCapabilities, type QueryEngineEvaluateRequest, @@ -293,6 +293,49 @@ describe("makeQueryEngineExecute", () => { }), ) + it.effect("reads trace_list_mv when the cluster lacks trace_facets_hourly", () => + Effect.gen(function* () { + const queries: Array = [] + const execute = makeQueryEngineExecute( + makeTinybirdStub({ + sqlQuery: (_tenant, sql) => { + queries.push(sql) + return sql.includes("trace_facets_hourly") + ? Effect.fail( + new WarehouseConfigError({ + message: "Unknown table expression identifier 'trace_facets_hourly'", + pipeName: "tracesDurationStats", + clickhouseType: "UNKNOWN_TABLE", + }), + ) + : Effect.succeed([ + { + minDurationMs: 1, + maxDurationMs: 9, + p50DurationMs: 4, + p95DurationMs: 8, + }, + ]) + }, + }), + ) + + const response = yield* execute(tenant, { + startTime: "2026-01-01 00:00:00", + endTime: "2026-01-08 00:00:00", + query: { kind: "stats", source: "traces" }, + }) + + assert.strictEqual(queries.length, 2) + assert.ok(!queries[1]!.includes("trace_facets_hourly")) + assert.deepStrictEqual(response.result, { + kind: "stats", + source: "traces", + data: { minDurationMs: 1, maxDurationMs: 9, p50DurationMs: 4, p95DurationMs: 8 }, + }) + }), + ) + it.effect("fills missing buckets while preserving existing traces values", () => Effect.gen(function* () { const execute = makeQueryEngineExecute( diff --git a/packages/backend/src/services/warehouse/trace-facets-hourly-materialization.clickhouse.e2e.test.ts b/packages/backend/src/services/warehouse/trace-facets-hourly-materialization.clickhouse.e2e.test.ts index f9a16e3c43..b87369b49a 100644 --- a/packages/backend/src/services/warehouse/trace-facets-hourly-materialization.clickhouse.e2e.test.ts +++ b/packages/backend/src/services/warehouse/trace-facets-hourly-materialization.clickhouse.e2e.test.ts @@ -8,6 +8,8 @@ import { afterAll, assert, beforeAll, describe, it } from "@effect/vitest" import { migrations, renderStatementFull } from "@maple/domain/clickhouse" +import * as CH from "@maple/query-engine/ch" +import { normalizeSqlForClickHouseClient } from "@maple/query-engine/execution" import { applyRealMigrations, clickhouseE2eEnabled, @@ -53,7 +55,13 @@ const BATCH_1: ReadonlyArray = [ // Not a root span: never reaches trace_list_mv, so never the rollup. { traceId: "t1", ms: BASE_MS + 61_000, service: "api", durationNs: 1_000, parentSpanId: "root-t1" }, ] -const BATCH_2: ReadonlyArray = [root("t5", BASE_MS + 240_000, "api", 3_000_000)] +const BATCH_2: ReadonlyArray = [ + root("t5", BASE_MS + 240_000, "api", 3_000_000), + // The longest span sits in a whole hour, so only the hourly tier can report it. + root("t6", BASE_MS + HOUR_MS + 120_000, "api", 80_000_000), + root("t7", BASE_MS + 2 * HOUR_MS + 300_000, "api", 4_000_000), + root("t8", BASE_MS + 2 * HOUR_MS + 900_000, "api", 6_000_000), +] const insert = async (spans: ReadonlyArray): Promise => { const rows = spans @@ -114,7 +122,7 @@ describe.skipIf(!clickhouseE2eEnabled)("trace_facets_hourly materialization", () const expected = await fromTraceList() assert.strictEqual( expected.reduce((total, row) => total + Number(row.traces), 0), - 5, + 8, ) assert.deepStrictEqual(await fromRollup(), expected) @@ -125,4 +133,37 @@ describe.skipIf(!clickhouseE2eEnabled)("trace_facets_hourly materialization", () await clickhouseExec(renderStatementFull(backfill, database), database) assert.deepStrictEqual(await fromRollup(), expected) }) + + // Starts mid-hour after t1 and ends mid-hour between t7 and t8, so both raw + // edges hold rows in and out of the window; the aligned window has an empty + // raw tier, whose zero extremes must not reach the result. + it("answers the sidebar identically from the splice and from trace_list_mv alone", async () => { + const run = (sql: string) => runJson(normalizeSqlForClickHouseClient(sql)) + const byFacet = (rows: ReadonlyArray>) => + [...rows].sort((a, b) => `${a.facetType}:${a.name}`.localeCompare(`${b.facetType}:${b.name}`)) + + for (const [startMs, endMs] of [ + [BASE_MS + 90_000, BASE_MS + 2 * HOUR_MS + 600_000], + [BASE_MS + HOUR_MS, BASE_MS + 2 * HOUR_MS], + ] as const) { + const window = { orgId: ORG_ID, startTime: chDateTime(startMs), endTime: chDateTime(endMs) } + const facets = (rawOnly: boolean) => + CH.compileUnionUnsafe(CH.tracesFacetsQuery({ rawOnly }), window).sql + const stats = (rawOnly: boolean) => + CH.compileUnsafe(CH.tracesDurationStatsQuery({ rawOnly }), window).sql + assert.include(facets(false), "trace_facets_hourly") + assert.notInclude(facets(true), "trace_facets_hourly") + assert.notInclude(stats(true), "trace_facets_hourly") + + assert.deepStrictEqual(byFacet(await run(facets(false))), byFacet(await run(facets(true)))) + const [spliced] = await run(stats(false)) + const [raw] = await run(stats(true)) + assert.strictEqual(spliced!.minDurationMs, raw!.minDurationMs) + assert.strictEqual(spliced!.maxDurationMs, 80) + assert.strictEqual(raw!.maxDurationMs, 80) + for (const key of ["p50DurationMs", "p95DurationMs"]) { + assert.closeTo(Number(spliced![key]), Number(raw![key]), Number(raw![key]) * 0.01) + } + } + }) }) diff --git a/packages/query-engine/src/__sql_baseline__/catalog.sql b/packages/query-engine/src/__sql_baseline__/catalog.sql index 600fc1d264..7cc21615e9 100644 --- a/packages/query-engine/src/__sql_baseline__/catalog.sql +++ b/packages/query-engine/src/__sql_baseline__/catalog.sql @@ -683,6 +683,255 @@ SELECT LIMIT 1 FORMAT JSON +-- builder:errors:tracesDurationStatsQuery:rollup [b8a8d457] +SELECT + minIf(durationMin, traceCount > 0) / 1000000 AS minDurationMs, + maxIf(durationMax, traceCount > 0) / 1000000 AS maxDurationMs, + ifNull(ifNotFinite(arrayElement(quantilesTDigestMerge(0.5, 0.95)(durationQuantiles), 1) / 1000000, 0), 0) AS p50DurationMs, + ifNull(ifNotFinite(arrayElement(quantilesTDigestMerge(0.5, 0.95)(durationQuantiles), 2) / 1000000, 0), 0) AS p95DurationMs + FROM ( +SELECT + count() AS traceCount, + min(Duration) AS durationMin, + max(Duration) AS durationMax, + quantilesTDigestState(0.5, 0.95)(Duration) AS durationQuantiles + FROM trace_list_mv + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) +UNION ALL +SELECT + sum(TraceCount) AS traceCount, + min(DurationMin) AS durationMin, + max(DurationMax) AS durationMax, + quantilesTDigestMergeState(0.5, 0.95)(DurationQuantiles) AS durationQuantiles + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND ServiceName = 'api' +) AS duration_tiers + FORMAT JSON + +-- builder:errors:tracesFacetsQuery:rollup [39248faa] +SELECT + name AS name, + sum(count) AS count, + 'service' AS facetType + FROM ( +SELECT + ServiceName AS name, + count() AS count + FROM trace_list_mv + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + GROUP BY name +UNION ALL +SELECT + ServiceName AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND ServiceName = 'api' + GROUP BY name +) AS service_tiers + GROUP BY name + ORDER BY count DESC + LIMIT 50 +UNION ALL +SELECT + name AS name, + sum(count) AS count, + 'spanName' AS facetType + FROM ( +SELECT + SpanName AS name, + count() AS count + FROM trace_list_mv + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND SpanName != '' + GROUP BY name +UNION ALL +SELECT + SpanName AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND ServiceName = 'api' + AND SpanName != '' + GROUP BY name +) AS spanName_tiers + GROUP BY name + ORDER BY count DESC + LIMIT 20 +UNION ALL +SELECT + name AS name, + sum(count) AS count, + 'httpMethod' AS facetType + FROM ( +SELECT + HttpMethod AS name, + count() AS count + FROM trace_list_mv + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND HttpMethod != '' + GROUP BY name +UNION ALL +SELECT + HttpMethod AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND ServiceName = 'api' + AND HttpMethod != '' + GROUP BY name +) AS httpMethod_tiers + GROUP BY name + ORDER BY count DESC + LIMIT 20 +UNION ALL +SELECT + name AS name, + sum(count) AS count, + 'httpStatus' AS facetType + FROM ( +SELECT + HttpStatusCode AS name, + count() AS count + FROM trace_list_mv + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND HttpStatusCode != '' + GROUP BY name +UNION ALL +SELECT + HttpStatusCode AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND ServiceName = 'api' + AND HttpStatusCode != '' + GROUP BY name +) AS httpStatus_tiers + GROUP BY name + ORDER BY count DESC + LIMIT 20 +UNION ALL +SELECT + name AS name, + sum(count) AS count, + 'deploymentEnv' AS facetType + FROM ( +SELECT + DeploymentEnv AS name, + count() AS count + FROM trace_list_mv + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND DeploymentEnv != '' + GROUP BY name +UNION ALL +SELECT + DeploymentEnv AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND ServiceName = 'api' + AND DeploymentEnv != '' + GROUP BY name +) AS deploymentEnv_tiers + GROUP BY name + ORDER BY count DESC + LIMIT 20 +UNION ALL +SELECT + name AS name, + sum(count) AS count, + 'serviceNamespace' AS facetType + FROM ( +SELECT + ServiceNamespace AS name, + count() AS count + FROM trace_list_mv + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND ServiceNamespace != '' + GROUP BY name +UNION ALL +SELECT + ServiceNamespace AS name, + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND ServiceName = 'api' + AND ServiceNamespace != '' + GROUP BY name +) AS serviceNamespace_tiers + GROUP BY name + ORDER BY count DESC + LIMIT 20 +UNION ALL +SELECT + 'error' AS name, + sum(count) AS count, + 'errorCount' AS facetType + FROM ( +SELECT + count() AS count + FROM trace_list_mv + WHERE OrgId = 'org_sql_catalog' + AND Timestamp >= '2026-01-01 10:30:00' + AND Timestamp <= '2026-01-03 14:15:00' + AND ServiceName = 'api' + AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) + AND HasError = 1 +UNION ALL +SELECT + sum(TraceCount) AS count + FROM trace_facets_hourly + WHERE OrgId = 'org_sql_catalog' + AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) + AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) + AND ServiceName = 'api' + AND HasError = 1 +) AS errorCount_tiers +FORMAT JSON + -- builder:infra:hostGaugeTimeseriesQuery:default [7d9ca740] SELECT toStartOfInterval(TimeUnix, INTERVAL 300 SECOND) AS bucket, @@ -10349,7 +10598,7 @@ SELECT LIMIT 20 FORMAT JSON --- pipe:traces_duration_stats:default:baseline [05b1bc71] +-- pipe:traces_duration_stats:default:baseline [379189b9] SELECT minIf(durationMin, traceCount > 0) / 1000000 AS minDurationMs, maxIf(durationMax, traceCount > 0) / 1000000 AS maxDurationMs, @@ -10365,38 +10614,18 @@ SELECT WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' - AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) -UNION ALL +) AS duration_tiers + FORMAT JSON + +-- pipe:traces_facets:attribute-filtered:baseline [2e8577ae] SELECT - sum(TraceCount) AS traceCount, - min(DurationMin) AS durationMin, - max(DurationMax) AS durationMax, - quantilesTDigestMergeState(0.5, 0.95)(DurationQuantiles) AS durationQuantiles - FROM trace_facets_hourly - WHERE OrgId = 'org_sql_catalog' - AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) - AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) -) AS duration_tiers - FORMAT JSON - --- pipe:traces_duration_stats:duration-bounded:baseline [711880b8] -SELECT - min(Duration) / 1000000 AS minDurationMs, - max(Duration) / 1000000 AS maxDurationMs, - ifNull(ifNotFinite(quantile(0.5)(Duration) / 1000000, 0), 0) AS p50DurationMs, - ifNull(ifNotFinite(quantile(0.95)(Duration) / 1000000, 0), 0) AS p95DurationMs - FROM trace_list_mv - WHERE OrgId = 'org_sql_catalog' - AND Timestamp >= '2026-01-01 10:30:00' - AND Timestamp <= '2026-01-03 14:15:00' - AND Duration >= 100000000 - FORMAT JSON - --- pipe:traces_facets:attribute-filtered:baseline [952a9803] + name AS name, + sum(count) AS count, + 'service' AS facetType + FROM ( SELECT ServiceName AS name, - count() AS count, - 'service' AS facetType + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' @@ -10418,13 +10647,19 @@ SELECT AND t_res.Timestamp <= '2026-01-03 14:15:00' AND t_res.ResourceAttributes['host.name'] = 'web') GROUP BY name +) AS service_tiers + GROUP BY name ORDER BY count DESC LIMIT 50 UNION ALL SELECT - SpanName AS name, - count() AS count, + name AS name, + sum(count) AS count, 'spanName' AS facetType + FROM ( +SELECT + SpanName AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' @@ -10447,13 +10682,19 @@ SELECT AND t_res.ResourceAttributes['host.name'] = 'web') AND SpanName != '' GROUP BY name +) AS spanName_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - HttpMethod AS name, - count() AS count, + name AS name, + sum(count) AS count, 'httpMethod' AS facetType + FROM ( +SELECT + HttpMethod AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' @@ -10476,13 +10717,19 @@ SELECT AND t_res.ResourceAttributes['host.name'] = 'web') AND HttpMethod != '' GROUP BY name +) AS httpMethod_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - HttpStatusCode AS name, - count() AS count, + name AS name, + sum(count) AS count, 'httpStatus' AS facetType + FROM ( +SELECT + HttpStatusCode AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' @@ -10505,13 +10752,19 @@ SELECT AND t_res.ResourceAttributes['host.name'] = 'web') AND HttpStatusCode != '' GROUP BY name +) AS httpStatus_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - DeploymentEnv AS name, - count() AS count, + name AS name, + sum(count) AS count, 'deploymentEnv' AS facetType + FROM ( +SELECT + DeploymentEnv AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' @@ -10534,13 +10787,19 @@ SELECT AND t_res.ResourceAttributes['host.name'] = 'web') AND DeploymentEnv != '' GROUP BY name +) AS deploymentEnv_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - ServiceNamespace AS name, - count() AS count, + name AS name, + sum(count) AS count, 'serviceNamespace' AS facetType + FROM ( +SELECT + ServiceNamespace AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' @@ -10563,13 +10822,18 @@ SELECT AND t_res.ResourceAttributes['host.name'] = 'web') AND ServiceNamespace != '' GROUP BY name +) AS serviceNamespace_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT 'error' AS name, - count() AS count, + sum(count) AS count, 'errorCount' AS facetType + FROM ( +SELECT + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' @@ -10591,13 +10855,18 @@ SELECT AND t_res.Timestamp <= '2026-01-03 14:15:00' AND t_res.ResourceAttributes['host.name'] = 'web') AND HasError = 1 +) AS errorCount_tiers FORMAT JSON --- pipe:traces_facets:attribute-filtered:bloom [952a9803] +-- pipe:traces_facets:attribute-filtered:bloom [2e8577ae] SELECT - ServiceName AS name, - count() AS count, + name AS name, + sum(count) AS count, 'service' AS facetType + FROM ( +SELECT + ServiceName AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' @@ -10619,13 +10888,19 @@ SELECT AND t_res.Timestamp <= '2026-01-03 14:15:00' AND t_res.ResourceAttributes['host.name'] = 'web') GROUP BY name +) AS service_tiers + GROUP BY name ORDER BY count DESC LIMIT 50 UNION ALL SELECT - SpanName AS name, - count() AS count, + name AS name, + sum(count) AS count, 'spanName' AS facetType + FROM ( +SELECT + SpanName AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' @@ -10648,13 +10923,19 @@ SELECT AND t_res.ResourceAttributes['host.name'] = 'web') AND SpanName != '' GROUP BY name +) AS spanName_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - HttpMethod AS name, - count() AS count, + name AS name, + sum(count) AS count, 'httpMethod' AS facetType + FROM ( +SELECT + HttpMethod AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' @@ -10677,13 +10958,19 @@ SELECT AND t_res.ResourceAttributes['host.name'] = 'web') AND HttpMethod != '' GROUP BY name +) AS httpMethod_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - HttpStatusCode AS name, - count() AS count, + name AS name, + sum(count) AS count, 'httpStatus' AS facetType + FROM ( +SELECT + HttpStatusCode AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' @@ -10706,13 +10993,19 @@ SELECT AND t_res.ResourceAttributes['host.name'] = 'web') AND HttpStatusCode != '' GROUP BY name +) AS httpStatus_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - DeploymentEnv AS name, - count() AS count, + name AS name, + sum(count) AS count, 'deploymentEnv' AS facetType + FROM ( +SELECT + DeploymentEnv AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' @@ -10735,13 +11028,19 @@ SELECT AND t_res.ResourceAttributes['host.name'] = 'web') AND DeploymentEnv != '' GROUP BY name +) AS deploymentEnv_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - ServiceNamespace AS name, - count() AS count, + name AS name, + sum(count) AS count, 'serviceNamespace' AS facetType + FROM ( +SELECT + ServiceNamespace AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' @@ -10764,13 +11063,18 @@ SELECT AND t_res.ResourceAttributes['host.name'] = 'web') AND ServiceNamespace != '' GROUP BY name +) AS serviceNamespace_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT 'error' AS name, - count() AS count, + sum(count) AS count, 'errorCount' AS facetType + FROM ( +SELECT + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' @@ -10792,13 +11096,18 @@ SELECT AND t_res.Timestamp <= '2026-01-03 14:15:00' AND t_res.ResourceAttributes['host.name'] = 'web') AND HasError = 1 +) AS errorCount_tiers FORMAT JSON --- pipe:traces_facets:attribute-filtered:text [952a9803] +-- pipe:traces_facets:attribute-filtered:text [2e8577ae] SELECT - ServiceName AS name, - count() AS count, + name AS name, + sum(count) AS count, 'service' AS facetType + FROM ( +SELECT + ServiceName AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' @@ -10820,13 +11129,19 @@ SELECT AND t_res.Timestamp <= '2026-01-03 14:15:00' AND t_res.ResourceAttributes['host.name'] = 'web') GROUP BY name +) AS service_tiers + GROUP BY name ORDER BY count DESC LIMIT 50 UNION ALL SELECT - SpanName AS name, - count() AS count, + name AS name, + sum(count) AS count, 'spanName' AS facetType + FROM ( +SELECT + SpanName AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' @@ -10849,13 +11164,19 @@ SELECT AND t_res.ResourceAttributes['host.name'] = 'web') AND SpanName != '' GROUP BY name +) AS spanName_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - HttpMethod AS name, - count() AS count, + name AS name, + sum(count) AS count, 'httpMethod' AS facetType + FROM ( +SELECT + HttpMethod AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' @@ -10878,13 +11199,19 @@ SELECT AND t_res.ResourceAttributes['host.name'] = 'web') AND HttpMethod != '' GROUP BY name +) AS httpMethod_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - HttpStatusCode AS name, - count() AS count, + name AS name, + sum(count) AS count, 'httpStatus' AS facetType + FROM ( +SELECT + HttpStatusCode AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' @@ -10907,13 +11234,19 @@ SELECT AND t_res.ResourceAttributes['host.name'] = 'web') AND HttpStatusCode != '' GROUP BY name +) AS httpStatus_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - DeploymentEnv AS name, - count() AS count, + name AS name, + sum(count) AS count, 'deploymentEnv' AS facetType + FROM ( +SELECT + DeploymentEnv AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' @@ -10936,13 +11269,19 @@ SELECT AND t_res.ResourceAttributes['host.name'] = 'web') AND DeploymentEnv != '' GROUP BY name +) AS deploymentEnv_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT - ServiceNamespace AS name, - count() AS count, + name AS name, + sum(count) AS count, 'serviceNamespace' AS facetType + FROM ( +SELECT + ServiceNamespace AS name, + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' @@ -10965,13 +11304,18 @@ SELECT AND t_res.ResourceAttributes['host.name'] = 'web') AND ServiceNamespace != '' GROUP BY name +) AS serviceNamespace_tiers + GROUP BY name ORDER BY count DESC LIMIT 20 UNION ALL SELECT 'error' AS name, - count() AS count, + sum(count) AS count, 'errorCount' AS facetType + FROM ( +SELECT + count() AS count FROM trace_list_mv WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' @@ -10993,9 +11337,10 @@ SELECT AND t_res.Timestamp <= '2026-01-03 14:15:00' AND t_res.ResourceAttributes['host.name'] = 'web') AND HasError = 1 +) AS errorCount_tiers FORMAT JSON --- pipe:traces_facets:default:baseline [2008761a] +-- pipe:traces_facets:default:baseline [a8566199] SELECT name AS name, sum(count) AS count, @@ -11008,16 +11353,6 @@ SELECT WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' - AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) - GROUP BY name -UNION ALL -SELECT - ServiceName AS name, - sum(TraceCount) AS count - FROM trace_facets_hourly - WHERE OrgId = 'org_sql_catalog' - AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) - AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) GROUP BY name ) AS service_tiers GROUP BY name @@ -11036,17 +11371,6 @@ SELECT WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' - AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) - AND SpanName != '' - GROUP BY name -UNION ALL -SELECT - SpanName AS name, - sum(TraceCount) AS count - FROM trace_facets_hourly - WHERE OrgId = 'org_sql_catalog' - AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) - AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) AND SpanName != '' GROUP BY name ) AS spanName_tiers @@ -11066,17 +11390,6 @@ SELECT WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' - AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) - AND HttpMethod != '' - GROUP BY name -UNION ALL -SELECT - HttpMethod AS name, - sum(TraceCount) AS count - FROM trace_facets_hourly - WHERE OrgId = 'org_sql_catalog' - AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) - AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) AND HttpMethod != '' GROUP BY name ) AS httpMethod_tiers @@ -11096,17 +11409,6 @@ SELECT WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' - AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) - AND HttpStatusCode != '' - GROUP BY name -UNION ALL -SELECT - HttpStatusCode AS name, - sum(TraceCount) AS count - FROM trace_facets_hourly - WHERE OrgId = 'org_sql_catalog' - AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) - AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) AND HttpStatusCode != '' GROUP BY name ) AS httpStatus_tiers @@ -11126,17 +11428,6 @@ SELECT WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' - AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) - AND DeploymentEnv != '' - GROUP BY name -UNION ALL -SELECT - DeploymentEnv AS name, - sum(TraceCount) AS count - FROM trace_facets_hourly - WHERE OrgId = 'org_sql_catalog' - AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) - AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) AND DeploymentEnv != '' GROUP BY name ) AS deploymentEnv_tiers @@ -11156,17 +11447,6 @@ SELECT WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' - AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) - AND ServiceNamespace != '' - GROUP BY name -UNION ALL -SELECT - ServiceNamespace AS name, - sum(TraceCount) AS count - FROM trace_facets_hourly - WHERE OrgId = 'org_sql_catalog' - AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) - AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) AND ServiceNamespace != '' GROUP BY name ) AS serviceNamespace_tiers @@ -11185,20 +11465,11 @@ SELECT WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' - AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) - AND HasError = 1 -UNION ALL -SELECT - sum(TraceCount) AS count - FROM trace_facets_hourly - WHERE OrgId = 'org_sql_catalog' - AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) - AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) AND HasError = 1 ) AS errorCount_tiers FORMAT JSON --- pipe:traces_facets:default:bloom [2008761a] +-- pipe:traces_facets:default:bloom [a8566199] SELECT name AS name, sum(count) AS count, @@ -11211,16 +11482,6 @@ SELECT WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' - AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) - GROUP BY name -UNION ALL -SELECT - ServiceName AS name, - sum(TraceCount) AS count - FROM trace_facets_hourly - WHERE OrgId = 'org_sql_catalog' - AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) - AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) GROUP BY name ) AS service_tiers GROUP BY name @@ -11239,17 +11500,6 @@ SELECT WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' - AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) - AND SpanName != '' - GROUP BY name -UNION ALL -SELECT - SpanName AS name, - sum(TraceCount) AS count - FROM trace_facets_hourly - WHERE OrgId = 'org_sql_catalog' - AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) - AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) AND SpanName != '' GROUP BY name ) AS spanName_tiers @@ -11269,17 +11519,6 @@ SELECT WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' - AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) - AND HttpMethod != '' - GROUP BY name -UNION ALL -SELECT - HttpMethod AS name, - sum(TraceCount) AS count - FROM trace_facets_hourly - WHERE OrgId = 'org_sql_catalog' - AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) - AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) AND HttpMethod != '' GROUP BY name ) AS httpMethod_tiers @@ -11299,17 +11538,6 @@ SELECT WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' - AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) - AND HttpStatusCode != '' - GROUP BY name -UNION ALL -SELECT - HttpStatusCode AS name, - sum(TraceCount) AS count - FROM trace_facets_hourly - WHERE OrgId = 'org_sql_catalog' - AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) - AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) AND HttpStatusCode != '' GROUP BY name ) AS httpStatus_tiers @@ -11329,17 +11557,6 @@ SELECT WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' - AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) - AND DeploymentEnv != '' - GROUP BY name -UNION ALL -SELECT - DeploymentEnv AS name, - sum(TraceCount) AS count - FROM trace_facets_hourly - WHERE OrgId = 'org_sql_catalog' - AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) - AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) AND DeploymentEnv != '' GROUP BY name ) AS deploymentEnv_tiers @@ -11359,17 +11576,6 @@ SELECT WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' - AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) - AND ServiceNamespace != '' - GROUP BY name -UNION ALL -SELECT - ServiceNamespace AS name, - sum(TraceCount) AS count - FROM trace_facets_hourly - WHERE OrgId = 'org_sql_catalog' - AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) - AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) AND ServiceNamespace != '' GROUP BY name ) AS serviceNamespace_tiers @@ -11388,20 +11594,11 @@ SELECT WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' - AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) - AND HasError = 1 -UNION ALL -SELECT - sum(TraceCount) AS count - FROM trace_facets_hourly - WHERE OrgId = 'org_sql_catalog' - AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) - AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) AND HasError = 1 ) AS errorCount_tiers FORMAT JSON --- pipe:traces_facets:default:text [2008761a] +-- pipe:traces_facets:default:text [a8566199] SELECT name AS name, sum(count) AS count, @@ -11414,16 +11611,6 @@ SELECT WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' - AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) - GROUP BY name -UNION ALL -SELECT - ServiceName AS name, - sum(TraceCount) AS count - FROM trace_facets_hourly - WHERE OrgId = 'org_sql_catalog' - AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) - AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) GROUP BY name ) AS service_tiers GROUP BY name @@ -11442,17 +11629,6 @@ SELECT WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' - AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) - AND SpanName != '' - GROUP BY name -UNION ALL -SELECT - SpanName AS name, - sum(TraceCount) AS count - FROM trace_facets_hourly - WHERE OrgId = 'org_sql_catalog' - AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) - AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) AND SpanName != '' GROUP BY name ) AS spanName_tiers @@ -11472,17 +11648,6 @@ SELECT WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' - AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) - AND HttpMethod != '' - GROUP BY name -UNION ALL -SELECT - HttpMethod AS name, - sum(TraceCount) AS count - FROM trace_facets_hourly - WHERE OrgId = 'org_sql_catalog' - AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) - AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) AND HttpMethod != '' GROUP BY name ) AS httpMethod_tiers @@ -11502,17 +11667,6 @@ SELECT WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' - AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) - AND HttpStatusCode != '' - GROUP BY name -UNION ALL -SELECT - HttpStatusCode AS name, - sum(TraceCount) AS count - FROM trace_facets_hourly - WHERE OrgId = 'org_sql_catalog' - AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) - AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) AND HttpStatusCode != '' GROUP BY name ) AS httpStatus_tiers @@ -11532,17 +11686,6 @@ SELECT WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' - AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) - AND DeploymentEnv != '' - GROUP BY name -UNION ALL -SELECT - DeploymentEnv AS name, - sum(TraceCount) AS count - FROM trace_facets_hourly - WHERE OrgId = 'org_sql_catalog' - AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) - AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) AND DeploymentEnv != '' GROUP BY name ) AS deploymentEnv_tiers @@ -11562,17 +11705,6 @@ SELECT WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' - AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) - AND ServiceNamespace != '' - GROUP BY name -UNION ALL -SELECT - ServiceNamespace AS name, - sum(TraceCount) AS count - FROM trace_facets_hourly - WHERE OrgId = 'org_sql_catalog' - AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) - AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) AND ServiceNamespace != '' GROUP BY name ) AS serviceNamespace_tiers @@ -11591,349 +11723,10 @@ SELECT WHERE OrgId = 'org_sql_catalog' AND Timestamp >= '2026-01-01 10:30:00' AND Timestamp <= '2026-01-03 14:15:00' - AND (Timestamp < if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) OR Timestamp >= toStartOfHour(toDateTime('2026-01-03 14:15:00'))) - AND HasError = 1 -UNION ALL -SELECT - sum(TraceCount) AS count - FROM trace_facets_hourly - WHERE OrgId = 'org_sql_catalog' - AND Hour >= if(toDateTime('2026-01-01 10:30:00') = toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')), toStartOfHour(toDateTime('2026-01-01 10:30:00')) + INTERVAL 1 HOUR) - AND Hour < toStartOfHour(toDateTime('2026-01-03 14:15:00')) AND HasError = 1 ) AS errorCount_tiers FORMAT JSON --- pipe:traces_facets:duration-bounded:baseline [1e990ddf] -SELECT - ServiceName AS name, - count() AS count, - 'service' AS facetType - FROM trace_list_mv - WHERE OrgId = 'org_sql_catalog' - AND Timestamp >= '2026-01-01 10:30:00' - AND Timestamp <= '2026-01-03 14:15:00' - AND ServiceName = 'api' - AND Duration >= 100000000 - AND Duration <= 5000000000 - GROUP BY name - ORDER BY count DESC - LIMIT 50 -UNION ALL -SELECT - SpanName AS name, - count() AS count, - 'spanName' AS facetType - FROM trace_list_mv - WHERE OrgId = 'org_sql_catalog' - AND Timestamp >= '2026-01-01 10:30:00' - AND Timestamp <= '2026-01-03 14:15:00' - AND ServiceName = 'api' - AND Duration >= 100000000 - AND Duration <= 5000000000 - AND SpanName != '' - GROUP BY name - ORDER BY count DESC - LIMIT 20 -UNION ALL -SELECT - HttpMethod AS name, - count() AS count, - 'httpMethod' AS facetType - FROM trace_list_mv - WHERE OrgId = 'org_sql_catalog' - AND Timestamp >= '2026-01-01 10:30:00' - AND Timestamp <= '2026-01-03 14:15:00' - AND ServiceName = 'api' - AND Duration >= 100000000 - AND Duration <= 5000000000 - AND HttpMethod != '' - GROUP BY name - ORDER BY count DESC - LIMIT 20 -UNION ALL -SELECT - HttpStatusCode AS name, - count() AS count, - 'httpStatus' AS facetType - FROM trace_list_mv - WHERE OrgId = 'org_sql_catalog' - AND Timestamp >= '2026-01-01 10:30:00' - AND Timestamp <= '2026-01-03 14:15:00' - AND ServiceName = 'api' - AND Duration >= 100000000 - AND Duration <= 5000000000 - AND HttpStatusCode != '' - GROUP BY name - ORDER BY count DESC - LIMIT 20 -UNION ALL -SELECT - DeploymentEnv AS name, - count() AS count, - 'deploymentEnv' AS facetType - FROM trace_list_mv - WHERE OrgId = 'org_sql_catalog' - AND Timestamp >= '2026-01-01 10:30:00' - AND Timestamp <= '2026-01-03 14:15:00' - AND ServiceName = 'api' - AND Duration >= 100000000 - AND Duration <= 5000000000 - AND DeploymentEnv != '' - GROUP BY name - ORDER BY count DESC - LIMIT 20 -UNION ALL -SELECT - ServiceNamespace AS name, - count() AS count, - 'serviceNamespace' AS facetType - FROM trace_list_mv - WHERE OrgId = 'org_sql_catalog' - AND Timestamp >= '2026-01-01 10:30:00' - AND Timestamp <= '2026-01-03 14:15:00' - AND ServiceName = 'api' - AND Duration >= 100000000 - AND Duration <= 5000000000 - AND ServiceNamespace != '' - GROUP BY name - ORDER BY count DESC - LIMIT 20 -UNION ALL -SELECT - 'error' AS name, - count() AS count, - 'errorCount' AS facetType - FROM trace_list_mv - WHERE OrgId = 'org_sql_catalog' - AND Timestamp >= '2026-01-01 10:30:00' - AND Timestamp <= '2026-01-03 14:15:00' - AND ServiceName = 'api' - AND Duration >= 100000000 - AND Duration <= 5000000000 - AND HasError = 1 -FORMAT JSON - --- pipe:traces_facets:duration-bounded:bloom [1e990ddf] -SELECT - ServiceName AS name, - count() AS count, - 'service' AS facetType - FROM trace_list_mv - WHERE OrgId = 'org_sql_catalog' - AND Timestamp >= '2026-01-01 10:30:00' - AND Timestamp <= '2026-01-03 14:15:00' - AND ServiceName = 'api' - AND Duration >= 100000000 - AND Duration <= 5000000000 - GROUP BY name - ORDER BY count DESC - LIMIT 50 -UNION ALL -SELECT - SpanName AS name, - count() AS count, - 'spanName' AS facetType - FROM trace_list_mv - WHERE OrgId = 'org_sql_catalog' - AND Timestamp >= '2026-01-01 10:30:00' - AND Timestamp <= '2026-01-03 14:15:00' - AND ServiceName = 'api' - AND Duration >= 100000000 - AND Duration <= 5000000000 - AND SpanName != '' - GROUP BY name - ORDER BY count DESC - LIMIT 20 -UNION ALL -SELECT - HttpMethod AS name, - count() AS count, - 'httpMethod' AS facetType - FROM trace_list_mv - WHERE OrgId = 'org_sql_catalog' - AND Timestamp >= '2026-01-01 10:30:00' - AND Timestamp <= '2026-01-03 14:15:00' - AND ServiceName = 'api' - AND Duration >= 100000000 - AND Duration <= 5000000000 - AND HttpMethod != '' - GROUP BY name - ORDER BY count DESC - LIMIT 20 -UNION ALL -SELECT - HttpStatusCode AS name, - count() AS count, - 'httpStatus' AS facetType - FROM trace_list_mv - WHERE OrgId = 'org_sql_catalog' - AND Timestamp >= '2026-01-01 10:30:00' - AND Timestamp <= '2026-01-03 14:15:00' - AND ServiceName = 'api' - AND Duration >= 100000000 - AND Duration <= 5000000000 - AND HttpStatusCode != '' - GROUP BY name - ORDER BY count DESC - LIMIT 20 -UNION ALL -SELECT - DeploymentEnv AS name, - count() AS count, - 'deploymentEnv' AS facetType - FROM trace_list_mv - WHERE OrgId = 'org_sql_catalog' - AND Timestamp >= '2026-01-01 10:30:00' - AND Timestamp <= '2026-01-03 14:15:00' - AND ServiceName = 'api' - AND Duration >= 100000000 - AND Duration <= 5000000000 - AND DeploymentEnv != '' - GROUP BY name - ORDER BY count DESC - LIMIT 20 -UNION ALL -SELECT - ServiceNamespace AS name, - count() AS count, - 'serviceNamespace' AS facetType - FROM trace_list_mv - WHERE OrgId = 'org_sql_catalog' - AND Timestamp >= '2026-01-01 10:30:00' - AND Timestamp <= '2026-01-03 14:15:00' - AND ServiceName = 'api' - AND Duration >= 100000000 - AND Duration <= 5000000000 - AND ServiceNamespace != '' - GROUP BY name - ORDER BY count DESC - LIMIT 20 -UNION ALL -SELECT - 'error' AS name, - count() AS count, - 'errorCount' AS facetType - FROM trace_list_mv - WHERE OrgId = 'org_sql_catalog' - AND Timestamp >= '2026-01-01 10:30:00' - AND Timestamp <= '2026-01-03 14:15:00' - AND ServiceName = 'api' - AND Duration >= 100000000 - AND Duration <= 5000000000 - AND HasError = 1 -FORMAT JSON - --- pipe:traces_facets:duration-bounded:text [1e990ddf] -SELECT - ServiceName AS name, - count() AS count, - 'service' AS facetType - FROM trace_list_mv - WHERE OrgId = 'org_sql_catalog' - AND Timestamp >= '2026-01-01 10:30:00' - AND Timestamp <= '2026-01-03 14:15:00' - AND ServiceName = 'api' - AND Duration >= 100000000 - AND Duration <= 5000000000 - GROUP BY name - ORDER BY count DESC - LIMIT 50 -UNION ALL -SELECT - SpanName AS name, - count() AS count, - 'spanName' AS facetType - FROM trace_list_mv - WHERE OrgId = 'org_sql_catalog' - AND Timestamp >= '2026-01-01 10:30:00' - AND Timestamp <= '2026-01-03 14:15:00' - AND ServiceName = 'api' - AND Duration >= 100000000 - AND Duration <= 5000000000 - AND SpanName != '' - GROUP BY name - ORDER BY count DESC - LIMIT 20 -UNION ALL -SELECT - HttpMethod AS name, - count() AS count, - 'httpMethod' AS facetType - FROM trace_list_mv - WHERE OrgId = 'org_sql_catalog' - AND Timestamp >= '2026-01-01 10:30:00' - AND Timestamp <= '2026-01-03 14:15:00' - AND ServiceName = 'api' - AND Duration >= 100000000 - AND Duration <= 5000000000 - AND HttpMethod != '' - GROUP BY name - ORDER BY count DESC - LIMIT 20 -UNION ALL -SELECT - HttpStatusCode AS name, - count() AS count, - 'httpStatus' AS facetType - FROM trace_list_mv - WHERE OrgId = 'org_sql_catalog' - AND Timestamp >= '2026-01-01 10:30:00' - AND Timestamp <= '2026-01-03 14:15:00' - AND ServiceName = 'api' - AND Duration >= 100000000 - AND Duration <= 5000000000 - AND HttpStatusCode != '' - GROUP BY name - ORDER BY count DESC - LIMIT 20 -UNION ALL -SELECT - DeploymentEnv AS name, - count() AS count, - 'deploymentEnv' AS facetType - FROM trace_list_mv - WHERE OrgId = 'org_sql_catalog' - AND Timestamp >= '2026-01-01 10:30:00' - AND Timestamp <= '2026-01-03 14:15:00' - AND ServiceName = 'api' - AND Duration >= 100000000 - AND Duration <= 5000000000 - AND DeploymentEnv != '' - GROUP BY name - ORDER BY count DESC - LIMIT 20 -UNION ALL -SELECT - ServiceNamespace AS name, - count() AS count, - 'serviceNamespace' AS facetType - FROM trace_list_mv - WHERE OrgId = 'org_sql_catalog' - AND Timestamp >= '2026-01-01 10:30:00' - AND Timestamp <= '2026-01-03 14:15:00' - AND ServiceName = 'api' - AND Duration >= 100000000 - AND Duration <= 5000000000 - AND ServiceNamespace != '' - GROUP BY name - ORDER BY count DESC - LIMIT 20 -UNION ALL -SELECT - 'error' AS name, - count() AS count, - 'errorCount' AS facetType - FROM trace_list_mv - WHERE OrgId = 'org_sql_catalog' - AND Timestamp >= '2026-01-01 10:30:00' - AND Timestamp <= '2026-01-03 14:15:00' - AND ServiceName = 'api' - AND Duration >= 100000000 - AND Duration <= 5000000000 - AND HasError = 1 -FORMAT JSON - -- spec:attribute-keys-logs:baseline [f2e10453] SELECT AttributeKey AS attributeKey, diff --git a/packages/query-engine/src/benchmark/builders.ts b/packages/query-engine/src/benchmark/builders.ts index 378a46c422..b6c8259e2e 100644 --- a/packages/query-engine/src/benchmark/builders.ts +++ b/packages/query-engine/src/benchmark/builders.ts @@ -754,6 +754,20 @@ export const builderFixtures: ReadonlyArray = [ ...webAnalyticsFixtures, ...webAnalyticsAiFixtures, + // Traces sidebar (query-engine runtime); the pipe fixtures cover the raw-only route. + { + module: "errors", + name: "tracesFacetsQuery", + label: "rollup", + compile: () => CH.compileUnionUnsafe(CH.tracesFacetsQuery({ serviceNames: ["api"] }), window), + }, + { + module: "errors", + name: "tracesDurationStatsQuery", + label: "rollup", + compile: () => CH.compileUnsafe(CH.tracesDurationStatsQuery({ serviceNames: ["api"] }), window), + }, + // Error fixtures reached through ErrorsService, v2 telemetry, and observability. { // telemetry.http.ts v2GetSpan / observability/span-detail.ts diff --git a/packages/query-engine/src/benchmark/catalog.test.ts b/packages/query-engine/src/benchmark/catalog.test.ts index d843292942..c7d3c827c6 100644 --- a/packages/query-engine/src/benchmark/catalog.test.ts +++ b/packages/query-engine/src/benchmark/catalog.test.ts @@ -306,8 +306,6 @@ const EXEMPT_BUILDERS: ReadonlySet = new Set([ "errors/errorsByTypeQuery", "errors/errorsTimeseriesQuery", "errors/spanHierarchyQuery", - "errors/tracesDurationStatsQuery", - "errors/tracesFacetsQuery", "errors/errorsFacetsQuery", "errors/errorsSummaryQuery", "errors/errorDetailTracesQuery", diff --git a/packages/query-engine/src/benchmark/catalog.ts b/packages/query-engine/src/benchmark/catalog.ts index 08b5c8fd71..f36be0924e 100644 --- a/packages/query-engine/src/benchmark/catalog.ts +++ b/packages/query-engine/src/benchmark/catalog.ts @@ -146,14 +146,7 @@ export const pipeFixtures: ReadonlyArray = [ params: { trace_id: TRACE_ID, start_time: undefined, end_time: undefined }, }, { pipe: "traces_duration_stats", label: "default", params: {} }, - { pipe: "traces_duration_stats", label: "duration-bounded", params: { min_duration_ms: 100 } }, { pipe: "traces_facets", label: "default", params: {}, allCapabilities: true }, - { - pipe: "traces_facets", - label: "duration-bounded", - params: { service: "api", min_duration_ms: 100, max_duration_ms: 5000 }, - allCapabilities: true, - }, { pipe: "traces_facets", label: "attribute-filtered", @@ -1283,22 +1276,6 @@ export function routeCoverage(): ReadonlyMap - record( - "canUseTraceFacetsRollup", - CH.canUseTraceFacetsRollup({ - minDurationMs: params.min_duration_ms as number | undefined, - maxDurationMs: params.max_duration_ms as number | undefined, - attributeFilterKey: params.attribute_filter_key as string | undefined, - resourceFilterKey: params.resource_filter_key as string | undefined, - }), - ) - } - // `useProductEvents` picks the page-view source: the `product_events` rollup, or raw // `session_events`. Read off the fixture labels rather than re-deriving it — // `webAnalyticsVariants` emits every web-analytics fixture in both variants diff --git a/packages/query-engine/src/ch/ch.test.ts b/packages/query-engine/src/ch/ch.test.ts index c97b61da80..6c1d63a28d 100644 --- a/packages/query-engine/src/ch/ch.test.ts +++ b/packages/query-engine/src/ch/ch.test.ts @@ -1332,8 +1332,6 @@ describe("converted queries", () => { }) const { sql } = compileUnsafe(q, baseParams) expect(sql).toContain("positionCaseInsensitive(ServiceName, 'api') > 0") - expect(sql).toContain("quantilesTDigestMergeState(0.5, 0.95)(DurationQuantiles)") - expect(sql).toContain("FROM trace_facets_hourly") }) it("spanHierarchyQuery projects only the trimmed tree attribute keys", () => { diff --git a/packages/query-engine/src/ch/pipe-dispatch.ts b/packages/query-engine/src/ch/pipe-dispatch.ts index 699e3e8cff..b8acef8306 100644 --- a/packages/query-engine/src/ch/pipe-dispatch.ts +++ b/packages/query-engine/src/ch/pipe-dispatch.ts @@ -257,6 +257,8 @@ export function compilePipeQuery( eraseType( compile( tracesDurationStatsQuery({ + // The pipe surface has no missing-table retry, so it keeps the raw read. + rawOnly: true, serviceName: str("service"), spanName: str("span_name"), hasError, @@ -281,6 +283,8 @@ export function compilePipeQuery( eraseType( compileUnion( tracesFacetsQuery({ + // The pipe surface has no missing-table retry, so it keeps the raw read. + rawOnly: true, serviceName: str("service"), spanName: str("span_name"), hasError, diff --git a/packages/query-engine/src/ch/queries/errors.test.ts b/packages/query-engine/src/ch/queries/errors.test.ts index bc3ae9537c..d05f00af5b 100644 --- a/packages/query-engine/src/ch/queries/errors.test.ts +++ b/packages/query-engine/src/ch/queries/errors.test.ts @@ -485,7 +485,6 @@ describe("tracesFacetsQuery", () => { const q = tracesFacetsQuery({}) const { sql } = compileUnionUnsafe(q, baseParams) // 7 facet branches, each splicing a raw edge with the hourly interior. - expect(sql.match(/\) AS \w+_tiers/g)).toHaveLength(7) expect(sql.match(/UNION ALL/g)).toHaveLength(6 + 7) expect(sql).toContain("'service' AS facetType") expect(sql).toContain("'spanName' AS facetType") @@ -570,9 +569,6 @@ describe("tracesFacetsQuery", () => { }) describe("trace facets rollup routing", () => { - const firstFullHour = "toStartOfHour(toDateTime('2024-01-01 00:00:00')) + INTERVAL 1 HOUR" - const endHour = "toStartOfHour(toDateTime('2024-01-02 00:00:00'))" - it("reads whole hours from trace_facets_hourly and the partial ends from trace_list_mv", () => { const filters = { serviceNames: ["api", "web"], hasError: true, deploymentEnvs: ["production"] } for (const sql of [ @@ -581,11 +577,6 @@ describe("trace facets rollup routing", () => { ]) { expect(sql).toContain("FROM trace_facets_hourly") expect(sql).toContain("FROM trace_list_mv") - // The rollup-splice boundary on both sides. - expect(sql).toContain(`Hour >= if(`) - expect(sql).toContain(`${firstFullHour})`) - expect(sql).toContain(`Hour < ${endHour}`) - expect(sql).toContain(`OR Timestamp >= ${endHour})`) // Every filter reaches both tiers. expect(sql.match(/ServiceName IN \('api', 'web'\)/g)).toHaveLength(2) expect(sql.match(/HasError = 1/g)).toHaveLength(2) @@ -593,16 +584,9 @@ describe("trace facets rollup routing", () => { } }) - it("merges duration state across the tiers, ignoring an empty tier's extremes", () => { - const { sql } = compileUnsafe(tracesDurationStatsQuery({}), baseParams) - expect(sql).toContain("quantilesTDigestState(0.5, 0.95)(Duration)") - expect(sql).toContain("quantilesTDigestMergeState(0.5, 0.95)(DurationQuantiles)") - expect(sql).toContain("minIf(durationMin, traceCount > 0)") - expect(sql).toContain("maxIf(durationMax, traceCount > 0)") - }) - - it("reads only trace_list_mv for filters the rollup does not carry", () => { + it("reads only trace_list_mv, for the whole window, when the rollup cannot answer", () => { for (const opts of [ + { rawOnly: true }, { minDurationMs: 100 }, { maxDurationMs: 100 }, { attributeFilterKey: "http.route", attributeFilterValue: "/users" }, @@ -610,10 +594,11 @@ describe("trace facets rollup routing", () => { ]) { const facets = compileUnionUnsafe(tracesFacetsQuery(opts), baseParams).sql expect(facets, JSON.stringify(opts)).not.toContain("trace_facets_hourly") + expect(facets, JSON.stringify(opts)).not.toContain("INTERVAL 1 HOUR") expect(facets.match(/UNION ALL/g), JSON.stringify(opts)).toHaveLength(6) } - const stats = compileUnsafe(tracesDurationStatsQuery({ minDurationMs: 100 }), baseParams).sql + const stats = compileUnsafe(tracesDurationStatsQuery({ rawOnly: true }), baseParams).sql expect(stats).not.toContain("trace_facets_hourly") - expect(stats).toContain("quantile(0.5)(Duration)") + expect(stats).not.toContain("INTERVAL 1 HOUR") }) }) diff --git a/packages/query-engine/src/ch/queries/errors.ts b/packages/query-engine/src/ch/queries/errors.ts index ef6dd176a1..3711f266e9 100644 --- a/packages/query-engine/src/ch/queries/errors.ts +++ b/packages/query-engine/src/ch/queries/errors.ts @@ -538,11 +538,10 @@ export function traceTimeProbeQuery(opts: { traceId: string; narrowByTime?: bool // Traces duration stats and facets // -// Both count root spans from `trace_list_mv` over the page's window. That table -// holds ~14M root spans a day for a busy org, so past about a day a scan of it -// exceeds the discovery budget. When the filters are ones `trace_facets_hourly` -// carries, its whole hours answer the interior of the window and -// `trace_list_mv` only the partial hour at each end (`rollup-splice`). +// Both count root spans over the window. When `trace_facets_hourly` carries the +// filters, its whole hours answer the interior and `trace_list_mv` only the +// partial hour at each end (`rollup-splice`); otherwise `trace_list_mv` answers +// the whole window through the same union. export interface TracesDurationStatsOpts { serviceName?: string @@ -570,6 +569,8 @@ export interface TracesDurationStatsOpts { deploymentEnv?: "contains" serviceNamespace?: "contains" } + /** Read only `trace_list_mv`: for clusters that have not applied migration 0034. */ + rawOnly?: boolean } /** The facet dimensions, spelled the same on `trace_list_mv` and `trace_facets_hourly`. */ @@ -613,10 +614,29 @@ function traceFacetDimensionConditions( ] } -function traceListWindowConditions( - $: ColumnAccessor, - opts: TracesDurationStatsOpts, -) { +/** + * Whether `trace_facets_hourly` can answer the whole-hour interior. It keeps the + * facet dimensions and nothing finer, so a duration bound or an attribute + * filter, which each need the individual root span, reads `trace_list_mv` for + * the whole window. + */ +export function canUseTraceFacetsRollup( + opts: TracesDurationStatsOpts & { + readonly attributeFilterKey?: string + readonly resourceFilterKey?: string + }, +): boolean { + return ( + !opts.rawOnly && + opts.minDurationMs == null && + opts.maxDurationMs == null && + !opts.attributeFilterKey && + !opts.resourceFilterKey + ) +} + +/** `trace_list_mv` rows in the window, narrowed to the partial end hours when the rollup has the rest. */ +function traceListWindowConditions($: ColumnAccessor, opts: TracesFacetsOpts) { return [ $.OrgId.eq(param.string("orgId")), $.Timestamp.gte(param.dateTimeSeconds("startTime")), @@ -624,6 +644,7 @@ function traceListWindowConditions( ...traceFacetDimensionConditions($, opts), CH.when(opts.minDurationMs, (v: number) => $.Duration.gte(v * 1000000)), CH.when(opts.maxDurationMs, (v: number) => $.Duration.lte(v * 1000000)), + CH.whenTrue(canUseTraceFacetsRollup(opts), () => edgeCondition("Timestamp")), ] } @@ -638,24 +659,13 @@ function traceFacetsHourlyInteriorConditions( ] } -/** - * Whether `trace_facets_hourly` can answer the whole-hour interior. It keeps the - * facet dimensions and nothing finer, so a duration bound or an attribute - * filter, which each need the individual root span, reads `trace_list_mv` for - * the whole window. - */ -export function canUseTraceFacetsRollup(opts: { - readonly minDurationMs?: number - readonly maxDurationMs?: number - readonly attributeFilterKey?: string - readonly resourceFilterKey?: string -}): boolean { - return ( - opts.minDurationMs == null && - opts.maxDurationMs == null && - !opts.attributeFilterKey && - !opts.resourceFilterKey - ) +/** The raw tier, plus the hourly interior when the rollup can answer it. */ +function traceFacetTiers>( + opts: TracesFacetsOpts, + raw: CHQuery, + hourly: () => CHQuery, +): CHUnionQuery { + return canUseTraceFacetsRollup(opts) ? unionAll(raw, hourly()) : unionAll(raw) } export interface TracesDurationStatsOutput { @@ -668,44 +678,32 @@ export interface TracesDurationStatsOutput { export function tracesDurationStatsQuery( opts: TracesDurationStatsOpts, ): CHQuery { - if (!canUseTraceFacetsRollup(opts)) { - return from(TraceListMv) - .select(($) => ({ - minDurationMs: CH.min_($.Duration).div(1000000), - maxDurationMs: CH.max_($.Duration).div(1000000), - p50DurationMs: finiteOrZero(CH.quantile(0.5)($.Duration).div(1000000)), - p95DurationMs: finiteOrZero(CH.quantile(0.95)($.Duration).div(1000000)), - })) - .where(($) => traceListWindowConditions($, opts)) - .format("JSON") - } - // Each tier reports its row count beside its extremes: an aggregate over an // empty tier returns 0 rather than nothing, and that 0 must not win the // outer `min`. The t-digest states merge across tiers; an empty one is inert. - const rawEdges = from(TraceListMv) + const raw = from(TraceListMv) .select(($) => ({ traceCount: CH.count(), durationMin: CH.min_($.Duration), durationMax: CH.max_($.Duration), durationQuantiles: CH.rawExpr("quantilesTDigestState(0.5, 0.95)(Duration)", T.string), })) - .where(($) => [...traceListWindowConditions($, opts), edgeCondition("Timestamp")]) - - const hourlyInterior = from(TraceFacetsHourly) - .select(($) => ({ - traceCount: CH.sum($.TraceCount), - durationMin: CH.min_($.DurationMin), - durationMax: CH.max_($.DurationMax), - durationQuantiles: CH.rawExpr( - "quantilesTDigestMergeState(0.5, 0.95)(DurationQuantiles)", - T.string, - ), - })) - .where(($) => traceFacetsHourlyInteriorConditions($, opts)) + .where(($) => traceListWindowConditions($, opts)) + const hourly = () => + from(TraceFacetsHourly) + .select(($) => ({ + traceCount: CH.sum($.TraceCount), + durationMin: CH.min_($.DurationMin), + durationMax: CH.max_($.DurationMax), + durationQuantiles: CH.rawExpr( + "quantilesTDigestMergeState(0.5, 0.95)(DurationQuantiles)", + T.string, + ), + })) + .where(($) => traceFacetsHourlyInteriorConditions($, opts)) const quantiles = "quantilesTDigestMerge(0.5, 0.95)(durationQuantiles)" - return fromUnion(unionAll(rawEdges, hourlyInterior), "duration_tiers") + return fromUnion(traceFacetTiers(opts, raw, hourly), "duration_tiers") .select(() => ({ minDurationMs: CH.rawExpr("minIf(durationMin, traceCount > 0) / 1000000", T.float64), maxDurationMs: CH.rawExpr("maxIf(durationMax, traceCount > 0) / 1000000", T.float64), @@ -803,9 +801,6 @@ export function tracesFacetsQuery(opts: TracesFacetsOpts): CHUnionQuery - // `colName` is a real column of both tiers, so the accessor already knows how // it decodes — naming it as a `dynamicColumn` threw that away and cost every // facet branch its row schema. @@ -814,36 +809,20 @@ export function tracesFacetsQuery(opts: TracesFacetsOpts): CHUnionQuery { - if (!useRollup) { - return from(TraceListMv) - .select(($) => ({ - name: $[colName], - count: CH.count(), - facetType: CH.lit(facetType), - })) - .where(($) => [...rawWhere($), CH.whenTrue(dropEmpty, () => $[colName].neq(""))]) - .groupBy("name") - .orderBy(["count", "desc"]) - .limit(limit) - } - - const rawEdges = from(TraceListMv) + ) => { + const raw = from(TraceListMv) .select(($) => ({ name: $[colName], count: CH.count() })) - .where(($) => [ - ...rawWhere($), - edgeCondition("Timestamp"), - CH.whenTrue(dropEmpty, () => $[colName].neq("")), - ]) - .groupBy("name") - const hourlyInterior = from(TraceFacetsHourly) - .select(($) => ({ name: $[colName], count: CH.sum($.TraceCount) })) - .where(($) => [ - ...traceFacetsHourlyInteriorConditions($, opts), - CH.whenTrue(dropEmpty, () => $[colName].neq("")), - ]) + .where(($) => [...rawWhere($), CH.whenTrue(dropEmpty, () => $[colName].neq(""))]) .groupBy("name") - return fromUnion(unionAll(rawEdges, hourlyInterior), `${facetType}_tiers`) + const hourly = () => + from(TraceFacetsHourly) + .select(($) => ({ name: $[colName], count: CH.sum($.TraceCount) })) + .where(($) => [ + ...traceFacetsHourlyInteriorConditions($, opts), + CH.whenTrue(dropEmpty, () => $[colName].neq("")), + ]) + .groupBy("name") + return fromUnion(traceFacetTiers(opts, raw, hourly), `${facetType}_tiers`) .select(($) => ({ name: $.name, count: CH.sum($.count), @@ -854,24 +833,15 @@ export function tracesFacetsQuery(opts: TracesFacetsOpts): CHUnionQuery { - if (!useRollup) { - return from(TraceListMv) - .select(() => ({ - name: CH.lit("error"), - count: CH.count(), - facetType: CH.lit("errorCount"), - })) - .where(($) => [...rawWhere($), $.HasError.eq(1)]) - } - - const rawEdges = from(TraceListMv) + const errorCountQuery = () => { + const raw = from(TraceListMv) .select(() => ({ count: CH.count() })) - .where(($) => [...rawWhere($), edgeCondition("Timestamp"), $.HasError.eq(1)]) - const hourlyInterior = from(TraceFacetsHourly) - .select(($) => ({ count: CH.sum($.TraceCount) })) - .where(($) => [...traceFacetsHourlyInteriorConditions($, opts), $.HasError.eq(1)]) - return fromUnion(unionAll(rawEdges, hourlyInterior), "errorCount_tiers").select(($) => ({ + .where(($) => [...rawWhere($), $.HasError.eq(1)]) + const hourly = () => + from(TraceFacetsHourly) + .select(($) => ({ count: CH.sum($.TraceCount) })) + .where(($) => [...traceFacetsHourlyInteriorConditions($, opts), $.HasError.eq(1)]) + return fromUnion(traceFacetTiers(opts, raw, hourly), "errorCount_tiers").select(($) => ({ name: CH.lit("error"), count: CH.sum($.count), facetType: CH.lit("errorCount"), @@ -885,7 +855,7 @@ export function tracesFacetsQuery(opts: TracesFacetsOpts): CHUnionQuery makeFacetQuery("HttpStatusCode", "httpStatus", true, 20), deploymentEnv: () => makeFacetQuery("DeploymentEnv", "deploymentEnv", true, 20), serviceNamespace: () => makeFacetQuery("ServiceNamespace", "serviceNamespace", true, 20), - } satisfies Record FacetBranch> + } satisfies Record ReturnType> if (opts.facet) { return unionAll(facetBranches[opts.facet]()).format("JSON") diff --git a/packages/query-engine/src/ch/tables.ts b/packages/query-engine/src/ch/tables.ts index 239c6e2d3f..43d7231f09 100644 --- a/packages/query-engine/src/ch/tables.ts +++ b/packages/query-engine/src/ch/tables.ts @@ -203,9 +203,6 @@ export const TraceFacetsHourly = table("trace_facets_hourly", { TraceCount: T.uint64, DurationMin: T.uint64, DurationMax: T.uint64, - // AggregateFunction(quantilesTDigest(0.5, 0.95), UInt64) — opaque state, only - // ever touched via raw `...MergeState`/`...Merge` exprs. - DurationQuantiles: T.string, }) export const Logs = table("logs", { diff --git a/packages/query-engine/src/runtime/query-engine.ts b/packages/query-engine/src/runtime/query-engine.ts index 5107a59712..bb0a473b4e 100644 --- a/packages/query-engine/src/runtime/query-engine.ts +++ b/packages/query-engine/src/runtime/query-engine.ts @@ -1263,19 +1263,40 @@ export interface QueryEngineExecuteOptions { } /** - * A ClickHouse cluster that has not applied migration 0015 answers any read of - * `service_overview_minutely` with `UNKNOWN_TABLE`, and the table name is always - * in the message. Matching the name rather than the error tag keeps this working - * across every layer the warehouse error is re-wrapped by on its way here. + * A ClickHouse cluster that has not applied a rollup's migration answers any + * read of it with `UNKNOWN_TABLE`, and the table name is always in the message. + * Matching the name rather than the error tag keeps this working across every + * layer the warehouse error is re-wrapped by on its way here. */ -const isMissingServiceOverviewMinutely = (error: unknown): boolean => { - if (typeof error !== "object" || error === null) return false - const candidate = error as { readonly clickhouseType?: unknown; readonly message?: unknown } - if (typeof candidate.message === "string" && /service_overview_minutely/i.test(candidate.message)) { - return true +const isMissingRollup = + (table: RegExp) => + (error: unknown): boolean => { + if (typeof error !== "object" || error === null) return false + const candidate = error as { readonly clickhouseType?: unknown; readonly message?: unknown } + if (typeof candidate.message === "string" && table.test(candidate.message)) return true + return candidate.clickhouseType === "UNKNOWN_TABLE" } - return candidate.clickhouseType === "UNKNOWN_TABLE" -} + +/** Migration 0015. */ +const isMissingServiceOverviewMinutely = isMissingRollup(/service_overview_minutely/i) + +/** + * `trace_facets_hourly` ships in a `requiredForIngest: false` migration (0034), + * so a BYO cluster may not have it yet; the sidebar then reads `trace_list_mv`. + */ +const withTraceFacetsFallback = ( + orgId: string, + run: (rawOnly: boolean) => Effect.Effect, +): Effect.Effect => + run(false).pipe( + Effect.catch((error) => + isMissingRollup(/trace_facets_hourly/i)(error) + ? Effect.logWarning( + "trace_facets_hourly is absent on this cluster; reading trace_list_mv. Apply ClickHouse schema to restore the fast path.", + ).pipe(Effect.annotateLogs({ orgId }), Effect.andThen(run(true))) + : Effect.fail(error), + ), + ) export const makeQueryEngineExecute = (warehouse: QueryEngineWarehouse) => Effect.fn("QueryEngineService.execute")(function* ( @@ -2004,13 +2025,15 @@ export const makeQueryEngineExecute = (warehouse: QueryEn request.query.filters as Record | undefined, ) const facet = request.query.facet - const rows = yield* executeCHUnionQuery( - warehouse, - tenant, - CH.tracesFacetsQuery({ ...opts, facet }), - baseParams, - facet ? `tracesFacets:${facet}` : "tracesFacets", - "discovery", + const rows = yield* withTraceFacetsFallback(tenant.orgId, (rawOnly) => + executeCHUnionQuery( + warehouse, + tenant, + CH.tracesFacetsQuery({ ...opts, facet, rawOnly }), + baseParams, + facet ? `tracesFacets:${facet}` : "tracesFacets", + "discovery", + ), ) return new QueryEngineExecuteResponse({ result: { @@ -2130,12 +2153,14 @@ export const makeQueryEngineExecute = (warehouse: QueryEn const opts = extractTracesDurationStatsOpts( request.query.filters as Record | undefined, ) - const rows = yield* executeCHQuery( - warehouse, - tenant, - CH.tracesDurationStatsQuery(opts), - { orgId: tenant.orgId, startTime: request.startTime, endTime: request.endTime }, - "tracesDurationStats", + const rows = yield* withTraceFacetsFallback(tenant.orgId, (rawOnly) => + executeCHQuery( + warehouse, + tenant, + CH.tracesDurationStatsQuery({ ...opts, rawOnly }), + { orgId: tenant.orgId, startTime: request.startTime, endTime: request.endTime }, + "tracesDurationStats", + ), ) const row = rows[0] return new QueryEngineExecuteResponse({ From 82abd5b45a52ebd53d11971d922d9ba78711e36f Mon Sep 17 00:00:00 2001 From: JeremyFunk Date: Mon, 28 Sep 2026 10:42:00 +0200 Subject: [PATCH 5/7] fix(cli): truncate trace_facets_hourly before the local backfill so a resumed step cannot double it --- apps/cli/src/server/local-store-migrations/steps.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/cli/src/server/local-store-migrations/steps.ts b/apps/cli/src/server/local-store-migrations/steps.ts index ea4dd5ca55..93e9307cbd 100644 --- a/apps/cli/src/server/local-store-migrations/steps.ts +++ b/apps/cli/src/server/local-store-migrations/steps.ts @@ -1054,6 +1054,7 @@ export const LOCAL_STORE_STEPS: ReadonlyArray = [ }, { // Both objects are new; the backfill rolls up the root spans already retained. + // A resumed step re-runs afterBootstrap on the staged target, hence the truncate. id: "local-0023-to-0024-trace-facets-hourly", from: 23, to: 24, @@ -1062,6 +1063,7 @@ export const LOCAL_STORE_STEPS: ReadonlyArray = [ clonedBefore: "any DDL runs", afterBootstrap: [ backfill( + "TRUNCATE TABLE trace_facets_hourly", "INSERT INTO trace_facets_hourly (OrgId, Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError, TraceCount, DurationMin, DurationMax, DurationQuantiles) SELECT OrgId, toStartOfHour(Timestamp) AS Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError, count() AS TraceCount, min(Duration) AS DurationMin, max(Duration) AS DurationMax, quantilesTDigestState(0.5, 0.95)(Duration) AS DurationQuantiles FROM trace_list_mv GROUP BY OrgId, Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError", ), ], From f4a2d5c5c7f12e7007a66eba13ce258149851fe1 Mon Sep 17 00:00:00 2001 From: JeremyFunk Date: Mon, 28 Sep 2026 10:42:26 +0200 Subject: [PATCH 6/7] fix(cli): use the idempotent TRUNCATE ... IF EXISTS form the step table allows --- apps/cli/src/server/local-store-migrations/steps.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/cli/src/server/local-store-migrations/steps.ts b/apps/cli/src/server/local-store-migrations/steps.ts index 93e9307cbd..5edfdfbe89 100644 --- a/apps/cli/src/server/local-store-migrations/steps.ts +++ b/apps/cli/src/server/local-store-migrations/steps.ts @@ -1063,7 +1063,7 @@ export const LOCAL_STORE_STEPS: ReadonlyArray = [ clonedBefore: "any DDL runs", afterBootstrap: [ backfill( - "TRUNCATE TABLE trace_facets_hourly", + "TRUNCATE TABLE IF EXISTS trace_facets_hourly", "INSERT INTO trace_facets_hourly (OrgId, Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError, TraceCount, DurationMin, DurationMax, DurationQuantiles) SELECT OrgId, toStartOfHour(Timestamp) AS Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError, count() AS TraceCount, min(Duration) AS DurationMin, max(Duration) AS DurationMax, quantilesTDigestState(0.5, 0.95)(Duration) AS DurationQuantiles FROM trace_list_mv GROUP BY OrgId, Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError", ), ], From febf1914ed7fe0305c78107f979410f28a29c08d Mon Sep 17 00:00:00 2001 From: JeremyFunk Date: Mon, 28 Sep 2026 22:57:04 +0200 Subject: [PATCH 7/7] fix(traces): retry the sidebar on trace_list_mv only when the rollup is missing From the effect-review-v4 pass: - Retry only on the WarehouseConfigError that names trace_facets_hourly, and only when the read included the rollup. Before, any UNKNOWN_TABLE, or any error whose message named the table (a timeout reading it), reread the heavier raw table; a missing trace_list_mv logged a false cause. - Annotate the span with query.rollup.fallback, like the service_overview_minutely fallback, which is back to its original form. - Tests: the facets retry path beside stats, a timeout on the rollup that must surface without a retry, a filtered splice-vs-raw pass in the e2e with a failing staging root only the hourly tier holds, and the contains match on both tiers. --- .../warehouse/QueryEngineService.test.ts | 110 ++++++++++++------ ...rly-materialization.clickhouse.e2e.test.ts | 49 ++++---- packages/query-engine/src/ch/ch.test.ts | 3 +- .../query-engine/src/runtime/query-engine.ts | 102 +++++++++------- 4 files changed, 166 insertions(+), 98 deletions(-) diff --git a/packages/backend/src/services/warehouse/QueryEngineService.test.ts b/packages/backend/src/services/warehouse/QueryEngineService.test.ts index e73ee774c2..66e7b25ef6 100644 --- a/packages/backend/src/services/warehouse/QueryEngineService.test.ts +++ b/packages/backend/src/services/warehouse/QueryEngineService.test.ts @@ -4,7 +4,12 @@ import { TestClock } from "effect/testing" import { Deferred, Effect, Exit, Fiber, Option, Schema } from "effect" import { strict as nodeAssert } from "node:assert" import { MetricName, OrgId, ServiceName, UserId } from "@maple/domain" -import { RawSqlValidationError, WarehouseConfigError, WarehouseUpstreamError } from "@maple/domain/http" +import { + RawSqlValidationError, + WarehouseConfigError, + WarehouseQuotaExceededError, + WarehouseUpstreamError, +} from "@maple/domain/http" import { baselineWarehouseCapabilities, type QueryEngineEvaluateRequest, @@ -293,46 +298,77 @@ describe("makeQueryEngineExecute", () => { }), ) + // The sidebar's two reads: a failure on the first one is what either returns. + const traceSidebarStub = (failRollup: () => unknown) => { + const queries: Array = [] + const execute = makeQueryEngineExecute( + makeTinybirdStub({ + sqlQuery: (_tenant, sql) => { + queries.push(sql) + return sql.includes("trace_facets_hourly") + ? Effect.fail(failRollup() as WarehouseConfigError) + : Effect.succeed( + sql.includes("facetType") + ? [{ name: "api", count: 3, facetType: "service" }] + : [ + { + minDurationMs: 1, + maxDurationMs: 9, + p50DurationMs: 4, + p95DurationMs: 8, + }, + ], + ) + }, + }), + ) + return { queries, execute } + } + const sidebarRequest = (kind: "facets" | "stats") => ({ + startTime: "2026-01-01 00:00:00", + endTime: "2026-01-08 00:00:00", + query: { kind, source: "traces" as const }, + }) + it.effect("reads trace_list_mv when the cluster lacks trace_facets_hourly", () => Effect.gen(function* () { - const queries: Array = [] - const execute = makeQueryEngineExecute( - makeTinybirdStub({ - sqlQuery: (_tenant, sql) => { - queries.push(sql) - return sql.includes("trace_facets_hourly") - ? Effect.fail( - new WarehouseConfigError({ - message: "Unknown table expression identifier 'trace_facets_hourly'", - pipeName: "tracesDurationStats", - clickhouseType: "UNKNOWN_TABLE", - }), - ) - : Effect.succeed([ - { - minDurationMs: 1, - maxDurationMs: 9, - p50DurationMs: 4, - p95DurationMs: 8, - }, - ]) - }, - }), - ) + for (const kind of ["facets", "stats"] as const) { + const { queries, execute } = traceSidebarStub( + () => + new WarehouseConfigError({ + message: "Unknown table expression identifier 'trace_facets_hourly'", + pipeName: "tracesFacets", + clickhouseType: "UNKNOWN_TABLE", + }), + ) + const response = yield* execute(tenant, sidebarRequest(kind)) - const response = yield* execute(tenant, { - startTime: "2026-01-01 00:00:00", - endTime: "2026-01-08 00:00:00", - query: { kind: "stats", source: "traces" }, - }) + assert.strictEqual(queries.length, 2, kind) + assert.ok(queries[0]!.includes("trace_facets_hourly"), kind) + assert.ok(!queries[1]!.includes("trace_facets_hourly"), kind) + assert.strictEqual(response.result.kind, kind) + } + }), + ) - assert.strictEqual(queries.length, 2) - assert.ok(!queries[1]!.includes("trace_facets_hourly")) - assert.deepStrictEqual(response.result, { - kind: "stats", - source: "traces", - data: { minDurationMs: 1, maxDurationMs: 9, p50DurationMs: 4, p95DurationMs: 8 }, - }) + it.effect("surfaces a rollup read that failed for another reason instead of rereading raw", () => + Effect.gen(function* () { + const { queries, execute } = traceSidebarStub( + () => + new WarehouseQuotaExceededError({ + message: "Timeout exceeded while reading from table default.trace_facets_hourly", + pipeName: "tracesFacets", + setting: "max_execution_time", + }), + ) + const exit = yield* Effect.exit(execute(tenant, sidebarRequest("facets"))) + + assert.isTrue(Exit.isFailure(exit)) + assert.strictEqual(queries.length, 1) + assert.strictEqual( + (getFailure(exit) as { _tag?: string } | undefined)?._tag, + "@maple/http/errors/WarehouseQuotaExceededError", + ) }), ) diff --git a/packages/backend/src/services/warehouse/trace-facets-hourly-materialization.clickhouse.e2e.test.ts b/packages/backend/src/services/warehouse/trace-facets-hourly-materialization.clickhouse.e2e.test.ts index b87369b49a..ca0d4323f5 100644 --- a/packages/backend/src/services/warehouse/trace-facets-hourly-materialization.clickhouse.e2e.test.ts +++ b/packages/backend/src/services/warehouse/trace-facets-hourly-materialization.clickhouse.e2e.test.ts @@ -61,6 +61,8 @@ const BATCH_2: ReadonlyArray = [ root("t6", BASE_MS + HOUR_MS + 120_000, "api", 80_000_000), root("t7", BASE_MS + 2 * HOUR_MS + 300_000, "api", 4_000_000), root("t8", BASE_MS + 2 * HOUR_MS + 900_000, "api", 6_000_000), + // The only failing staging root inside a whole hour: proves filters reach the hourly tier. + root("t9", BASE_MS + HOUR_MS + 180_000, "worker", 30_000_000, { status: "Error", env: "staging" }), ] const insert = async (spans: ReadonlyArray): Promise => { @@ -122,7 +124,7 @@ describe.skipIf(!clickhouseE2eEnabled)("trace_facets_hourly materialization", () const expected = await fromTraceList() assert.strictEqual( expected.reduce((total, row) => total + Number(row.traces), 0), - 8, + 9, ) assert.deepStrictEqual(await fromRollup(), expected) @@ -142,27 +144,32 @@ describe.skipIf(!clickhouseE2eEnabled)("trace_facets_hourly materialization", () const byFacet = (rows: ReadonlyArray>) => [...rows].sort((a, b) => `${a.facetType}:${a.name}`.localeCompare(`${b.facetType}:${b.name}`)) - for (const [startMs, endMs] of [ - [BASE_MS + 90_000, BASE_MS + 2 * HOUR_MS + 600_000], - [BASE_MS + HOUR_MS, BASE_MS + 2 * HOUR_MS], + for (const [filters, longestMs] of [ + [{}, 80], + [{ hasError: true, deploymentEnvs: ["staging"] }, 30], ] as const) { - const window = { orgId: ORG_ID, startTime: chDateTime(startMs), endTime: chDateTime(endMs) } - const facets = (rawOnly: boolean) => - CH.compileUnionUnsafe(CH.tracesFacetsQuery({ rawOnly }), window).sql - const stats = (rawOnly: boolean) => - CH.compileUnsafe(CH.tracesDurationStatsQuery({ rawOnly }), window).sql - assert.include(facets(false), "trace_facets_hourly") - assert.notInclude(facets(true), "trace_facets_hourly") - assert.notInclude(stats(true), "trace_facets_hourly") - - assert.deepStrictEqual(byFacet(await run(facets(false))), byFacet(await run(facets(true)))) - const [spliced] = await run(stats(false)) - const [raw] = await run(stats(true)) - assert.strictEqual(spliced!.minDurationMs, raw!.minDurationMs) - assert.strictEqual(spliced!.maxDurationMs, 80) - assert.strictEqual(raw!.maxDurationMs, 80) - for (const key of ["p50DurationMs", "p95DurationMs"]) { - assert.closeTo(Number(spliced![key]), Number(raw![key]), Number(raw![key]) * 0.01) + for (const [startMs, endMs] of [ + [BASE_MS + 90_000, BASE_MS + 2 * HOUR_MS + 600_000], + [BASE_MS + HOUR_MS, BASE_MS + 2 * HOUR_MS], + ] as const) { + const window = { orgId: ORG_ID, startTime: chDateTime(startMs), endTime: chDateTime(endMs) } + const facets = (rawOnly: boolean) => + CH.compileUnionUnsafe(CH.tracesFacetsQuery({ ...filters, rawOnly }), window).sql + const stats = (rawOnly: boolean) => + CH.compileUnsafe(CH.tracesDurationStatsQuery({ ...filters, rawOnly }), window).sql + assert.include(facets(false), "trace_facets_hourly") + assert.notInclude(facets(true), "trace_facets_hourly") + assert.notInclude(stats(true), "trace_facets_hourly") + + assert.deepStrictEqual(byFacet(await run(facets(false))), byFacet(await run(facets(true)))) + const [spliced] = await run(stats(false)) + const [raw] = await run(stats(true)) + assert.strictEqual(spliced!.minDurationMs, raw!.minDurationMs) + assert.strictEqual(spliced!.maxDurationMs, longestMs) + assert.strictEqual(raw!.maxDurationMs, longestMs) + for (const key of ["p50DurationMs", "p95DurationMs"]) { + assert.closeTo(Number(spliced![key]), Number(raw![key]), Number(raw![key]) * 0.01) + } } } }) diff --git a/packages/query-engine/src/ch/ch.test.ts b/packages/query-engine/src/ch/ch.test.ts index 6c1d63a28d..faa76524b2 100644 --- a/packages/query-engine/src/ch/ch.test.ts +++ b/packages/query-engine/src/ch/ch.test.ts @@ -1331,7 +1331,8 @@ describe("converted queries", () => { matchModes: { serviceName: "contains" }, }) const { sql } = compileUnsafe(q, baseParams) - expect(sql).toContain("positionCaseInsensitive(ServiceName, 'api') > 0") + // On both tiers: the trace_list_mv edges and the hourly interior. + expect(sql.match(/positionCaseInsensitive\(ServiceName, 'api'\) > 0/g)).toHaveLength(2) }) it("spanHierarchyQuery projects only the trimmed tree attribute keys", () => { diff --git a/packages/query-engine/src/runtime/query-engine.ts b/packages/query-engine/src/runtime/query-engine.ts index bb0a473b4e..818341fd03 100644 --- a/packages/query-engine/src/runtime/query-engine.ts +++ b/packages/query-engine/src/runtime/query-engine.ts @@ -1263,40 +1263,58 @@ export interface QueryEngineExecuteOptions { } /** - * A ClickHouse cluster that has not applied a rollup's migration answers any - * read of it with `UNKNOWN_TABLE`, and the table name is always in the message. - * Matching the name rather than the error tag keeps this working across every - * layer the warehouse error is re-wrapped by on its way here. + * A ClickHouse cluster that has not applied migration 0015 answers any read of + * `service_overview_minutely` with `UNKNOWN_TABLE`, and the table name is always + * in the message. Matching the name rather than the error tag keeps this working + * across every layer the warehouse error is re-wrapped by on its way here. */ -const isMissingRollup = - (table: RegExp) => - (error: unknown): boolean => { - if (typeof error !== "object" || error === null) return false - const candidate = error as { readonly clickhouseType?: unknown; readonly message?: unknown } - if (typeof candidate.message === "string" && table.test(candidate.message)) return true - return candidate.clickhouseType === "UNKNOWN_TABLE" +const isMissingServiceOverviewMinutely = (error: unknown): boolean => { + if (typeof error !== "object" || error === null) return false + const candidate = error as { readonly clickhouseType?: unknown; readonly message?: unknown } + if (typeof candidate.message === "string" && /service_overview_minutely/i.test(candidate.message)) { + return true } + return candidate.clickhouseType === "UNKNOWN_TABLE" +} -/** Migration 0015. */ -const isMissingServiceOverviewMinutely = isMissingRollup(/service_overview_minutely/i) +/** + * The missing-table config error naming the rollup. A timeout or memory error + * on the rollup read also names it, and must surface rather than retry against + * the heavier raw table. + */ +const isMissingTraceFacetsRollup = (error: unknown): boolean => { + if (typeof error !== "object" || error === null) return false + const candidate = error as { readonly _tag?: unknown; readonly message?: unknown } + return ( + candidate._tag === "@maple/http/errors/WarehouseConfigError" && + typeof candidate.message === "string" && + /trace_facets_hourly/i.test(candidate.message) + ) +} /** * `trace_facets_hourly` ships in a `requiredForIngest: false` migration (0034), * so a BYO cluster may not have it yet; the sidebar then reads `trace_list_mv`. + * Only a read that actually included the rollup can be missing it. */ const withTraceFacetsFallback = ( orgId: string, + usesRollup: boolean, run: (rawOnly: boolean) => Effect.Effect, ): Effect.Effect => - run(false).pipe( - Effect.catch((error) => - isMissingRollup(/trace_facets_hourly/i)(error) - ? Effect.logWarning( - "trace_facets_hourly is absent on this cluster; reading trace_list_mv. Apply ClickHouse schema to restore the fast path.", - ).pipe(Effect.annotateLogs({ orgId }), Effect.andThen(run(true))) - : Effect.fail(error), - ), - ) + usesRollup + ? run(false).pipe( + Effect.catchIf(isMissingTraceFacetsRollup, () => + Effect.gen(function* () { + yield* Effect.logWarning( + "trace_facets_hourly is absent on this cluster; reading trace_list_mv. Apply ClickHouse schema to restore the fast path.", + ).pipe(Effect.annotateLogs({ orgId })) + yield* Effect.annotateCurrentSpan("query.rollup.fallback", true) + return yield* run(true) + }), + ), + ) + : run(true) export const makeQueryEngineExecute = (warehouse: QueryEngineWarehouse) => Effect.fn("QueryEngineService.execute")(function* ( @@ -2025,15 +2043,18 @@ export const makeQueryEngineExecute = (warehouse: QueryEn request.query.filters as Record | undefined, ) const facet = request.query.facet - const rows = yield* withTraceFacetsFallback(tenant.orgId, (rawOnly) => - executeCHUnionQuery( - warehouse, - tenant, - CH.tracesFacetsQuery({ ...opts, facet, rawOnly }), - baseParams, - facet ? `tracesFacets:${facet}` : "tracesFacets", - "discovery", - ), + const rows = yield* withTraceFacetsFallback( + tenant.orgId, + CH.canUseTraceFacetsRollup(opts), + (rawOnly) => + executeCHUnionQuery( + warehouse, + tenant, + CH.tracesFacetsQuery({ ...opts, facet, rawOnly }), + baseParams, + facet ? `tracesFacets:${facet}` : "tracesFacets", + "discovery", + ), ) return new QueryEngineExecuteResponse({ result: { @@ -2153,14 +2174,17 @@ export const makeQueryEngineExecute = (warehouse: QueryEn const opts = extractTracesDurationStatsOpts( request.query.filters as Record | undefined, ) - const rows = yield* withTraceFacetsFallback(tenant.orgId, (rawOnly) => - executeCHQuery( - warehouse, - tenant, - CH.tracesDurationStatsQuery({ ...opts, rawOnly }), - { orgId: tenant.orgId, startTime: request.startTime, endTime: request.endTime }, - "tracesDurationStats", - ), + const rows = yield* withTraceFacetsFallback( + tenant.orgId, + CH.canUseTraceFacetsRollup(opts), + (rawOnly) => + executeCHQuery( + warehouse, + tenant, + CH.tracesDurationStatsQuery({ ...opts, rawOnly }), + { orgId: tenant.orgId, startTime: request.startTime, endTime: request.endTime }, + "tracesDurationStats", + ), ) const row = rows[0] return new QueryEngineExecuteResponse({