Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .agents/skills/maple-telemetry-conventions/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Reference for the **language-agnostic** OpenTelemetry conventions Maple uses acr

- `rules/span-attributes.md` — Master reference of every custom attribute key Maple emits, grouped by namespace, with file:line citations.
- `rules/status-and-kind.md` — Title Case status code rule (`Ok`/`Error`/`Unset`) and span kind conventions (`Server` / `Client` / `Internal`).
- `rules/resource-attributes.md` — `service.*` identity, `deployment.environment.name` resolution order, the legacy `deployment.environment` dual-emit, and `maple_org_id`.
- `rules/resource-attributes.md` — `service.*` identity, `deployment.environment.name` resolution order, the deprecated `deployment.environment` dual-emit and read-side coalesce, and `maple_org_id`.
- `rules/language-bindings.md` — Parallel TypeScript / Rust / Python snippets that emit the same attribute keys.
- `rules/mv-first-class-columns.md` — Which span and resource attributes Tinybird MVs pre-extract into columns (and the rule for adding new ones).
- `rules/service-map-attribution.md` — Required span and resource attributes for the service map to render edges, runtime icons, and platform badges. Includes the canonical `peer.service` registry.
Expand All @@ -35,7 +35,7 @@ Reference for the **language-agnostic** OpenTelemetry conventions Maple uses acr
| Vendor namespace | Custom attributes go under `maple.*`. Sub-namespaces: `maple.ingest.*`, `maple.cloudflare.*`. |
| Standard semconv | Use OTel semconv keys verbatim: `service.name`, `http.request.method`, `db.system.name`, `error.type`. |
| Org identity | `orgId` (camelCase) in TypeScript spans, `maple.org_id` (dotted) in Rust spans. Don't unify until MVs migrate. |
| Deployment env | Dual-emit `deployment.environment` + `deployment.environment.name`. Keep both until MV `coalesce()` migration lands. |
| Deployment env | Emit `deployment.environment.name` (we dual-emit the deprecated `deployment.environment` too). **Read** both via `DEPLOYMENT_ENV_SQL` / `deploymentEnvExpr` — never a bare map lookup. |
| Warehouse SQL spans | Every span from `WarehouseQueryService.executeSql` carries `db.system.name`, `peer.service`, `db.query.text`, `db.query.fingerprint`, `db.duration_ms`, `result.rowCount`, `orgId`, `query.context`, `query.profile`. Legacy spans (pre 2026-06) use `db.statement*`/`db.system`; warehouse readers coalesce both. |
| Service map | Outbound spans need `peer.service` (HTTP/RPC) or `db.system.name` (DB) on a `Client`/`Producer` span. Resource attrs need `process.runtime.name`, `cloud.platform`, `maple.sdk.type` for runtime icon + platform badge. See `rules/service-map-attribution.md`. |
| Loop prevention | Never remove `HttpMiddleware.TracerDisabledWhen` (apps/api/src/app.ts:169-175) or the ingest loopback guard (apps/ingest/src/main.rs:499-514). |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ Lightweight projection of trace spans for the service dependency map.
| Column | Extracted from | Line |
|---|---|---|
| `PeerService` | `SpanAttributes['peer.service']` | `materializations.ts:246` |
| `DeploymentEnv` | `ResourceAttributes['deployment.environment']` | `materializations.ts:247` |
| `DeploymentEnv` | `DEPLOYMENT_ENV_SQL` | `materializations.ts:247` |

## `service_map_children_mv`

Spans with a parent, for child-of-edge analysis in the service map.

| Column | Extracted from | Line |
|---|---|---|
| `DeploymentEnv` | `ResourceAttributes['deployment.environment']` | `materializations.ts:315` |
| `DeploymentEnv` | `DEPLOYMENT_ENV_SQL` | `materializations.ts:315` |

## `service_map_edges_hourly_mv`

Expand All @@ -32,15 +32,15 @@ Pre-aggregated client-to-peer edges. Pre-filters to spans with `peer.service !=
| Column | Extracted from | Line |
|---|---|---|
| `TargetService` | `SpanAttributes['peer.service']` | `materializations.ts:341` |
| `DeploymentEnv` | `ResourceAttributes['deployment.environment']` | `materializations.ts:342` |
| `DeploymentEnv` | `DEPLOYMENT_ENV_SQL` | `materializations.ts:342` |

## `service_overview_spans_mv`

Hourly service-overview rollup.

| Column | Extracted from | Line |
|---|---|---|
| `DeploymentEnv` | `ResourceAttributes['deployment.environment']` | `materializations.ts:277` |
| `DeploymentEnv` | `DEPLOYMENT_ENV_SQL` | `materializations.ts:277` |
| `CommitSha` | `ResourceAttributes['deployment.commit_sha']` | `materializations.ts:278` (see file for the field) |

## `service_platforms_hourly_mv`
Expand All @@ -49,7 +49,7 @@ Per-service hosting-platform attributes for the service map's runtime-icon resol

| Column | Extracted from | Line |
|---|---|---|
| `DeploymentEnv` | `ResourceAttributes['deployment.environment']` | `materializations.ts:414` |
| `DeploymentEnv` | `DEPLOYMENT_ENV_SQL` | `materializations.ts:414` |
| `K8sCluster` | `max(ResourceAttributes['k8s.cluster.name'])` | `materializations.ts:415` |
| `K8sPodName` | `max(ResourceAttributes['k8s.pod.name'])` | `materializations.ts:416` |
| `K8sDeploymentName` | `max(ResourceAttributes['k8s.deployment.name'])` | `materializations.ts:417` |
Expand All @@ -69,7 +69,7 @@ Unwraps the first OTel `exception` event from `EventsName` / `EventsAttributes`
| `ExceptionStacktrace` | first `exception` event's `exception.stacktrace` | `materializations.ts:499` |
| `TopFrame` | (computed from stacktrace) | `materializations.ts` |
| `FingerprintHash` | cityHash64 of grouping keys | `materializations.ts:489` |
| `DeploymentEnv` | `ResourceAttributes['deployment.environment']` | `materializations.ts:525` |
| `DeploymentEnv` | `DEPLOYMENT_ENV_SQL` | `materializations.ts:525` |

## `trace_list_mv`

Expand All @@ -80,21 +80,21 @@ Trace list optimized for the trace search UI. Pre-filters to entry-point spans.
| `HttpMethod` | `SpanAttributes['http.method']` (fallback `http.request.method`) | `materializations.ts:596` |
| `HttpRoute` | `SpanAttributes['http.route']` (fallbacks `url.path`, `http.target`) | `materializations.ts:597` |
| `HttpStatusCode` | `SpanAttributes['http.status_code']` (fallback `http.response.status_code`) | `materializations.ts:598` |
| `DeploymentEnv` | `ResourceAttributes['deployment.environment']` | `materializations.ts:599` |
| `DeploymentEnv` | `DEPLOYMENT_ENV_SQL` | `materializations.ts:599` |

## `traces_aggregates_hourly_mv`

Hourly trace-shape rollup.

| Column | Extracted from | Line |
|---|---|---|
| `DeploymentEnv` | `ResourceAttributes['deployment.environment']` | `materializations.ts:778` |
| `DeploymentEnv` | `DEPLOYMENT_ENV_SQL` | `materializations.ts:778` |

Plus dimension keys pre-aggregated on the trace itself: `ServiceName`, `SpanName`, `SpanKind`, `StatusCode`, `IsEntryPoint`.

## `logs_aggregates_hourly_mv` (referenced)

Same pattern — extracts `DeploymentEnv` from `ResourceAttributes['deployment.environment']`. See `materializations.ts:809`.
Same pattern — extracts `DeploymentEnv` through `DEPLOYMENT_ENV_SQL`. See `materializations.ts:809`.

---

Expand All @@ -104,7 +104,7 @@ Same pattern — extracts `DeploymentEnv` from `ResourceAttributes['deployment.e

Example: if you add a new MV column `HttpUserAgent` that extracts `SpanAttributes['http.user_agent']`, every service emitting spans **must** use `http.user_agent` as the attribute key. Don't have one service emit `http.user_agent` and another `userAgent` — only one will populate the column.

Corollary: **the dual-emit rule for `deployment.environment` is here**. Every MV in this file extracts `ResourceAttributes['deployment.environment']` (the legacy key). Until those MVs migrate to `coalesce(deployment.environment.name, deployment.environment)`, the legacy resource attribute **must** be emitted alongside the new one. See `rules/resource-attributes.md`.
Corollary: every MV here extracts `DeploymentEnv` through the shared `DEPLOYMENT_ENV_SQL` fragment (`packages/domain/src/tinybird/semconv-renames.ts`), which coalesces `deployment.environment.name` over the deprecated `deployment.environment` — as `MESSAGING_DESTINATION_SQL` does for `messaging.destination(.name)` in the external-edge rollup. A bare lookup on either key alone silently materializes an empty environment for half the instrumentation in the wild. See `rules/resource-attributes.md`.

## When NOT to extract into a column

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,27 @@ Resource attributes are set **once per process** on the OTel `Resource` and appl
| `service.version` | string | build-time | `env!("CARGO_PKG_VERSION")` in Rust, package version in TS | Semantic version; used for release-correlation. |
| `service.instance.id` | string | runtime | `uuid::Uuid::new_v4().to_string()` per process | Per-process UUID generated at startup. Lets dashboards distinguish replicas. |

## Deployment environment — dual emit
## Deployment environment — read both, emit both

The most important rule in this file. Maple emits **both** the new and legacy keys:
OTel renamed this attribute; the registry lists `deployment.environment.name` as stable and plain `deployment.environment` as deprecated ("Replaced by `deployment.environment.name`").

| Key | Status |
|---|---|
| `deployment.environment.name` | OTel-canonical (new spec) |
| `deployment.environment` | Legacy — **keep until all Tinybird MVs migrate to `coalesce()` both keys** |
| `deployment.environment.name` | OTel-canonical — emit this one from new code |
| `deployment.environment` | Deprecated — Maple still emits it, and still **reads** it |

**Reading is the rule that matters.** Anything that pulls the environment out of `ResourceAttributes` — an MV body, a query-engine filter, a facet — uses the shared coalesce, never a bare map lookup:

```ts
import { DEPLOYMENT_ENV_SQL, deploymentEnvExpr } from "@maple/domain/tinybird/semconv-renames"
```

```sql
coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''),
ResourceAttributes['deployment.environment'])
```

A bare lookup on either key alone drops the environment for half the fleet: our own SDKs dual-emit, a current OTel SDK sends only `.name`, an older one only the legacy key. `packages/domain/src/tinybird/semconv-renames.test.ts` fails if any MV extracts `DeploymentEnv` some other way. The same module carries `MESSAGING_DESTINATION_SQL` for the `messaging.destination` -> `messaging.destination.name` rename; any future rename Maple *keys on* belongs there too.

Source: `apps/ingest/src/main.rs:526-538`

Expand All @@ -27,12 +40,10 @@ Source: `apps/ingest/src/main.rs:526-538`
"deployment.environment.name",
deployment_env.clone(),
))
// Dual-emit the legacy `deployment.environment` key: every Tinybird MV
// (service_overview_spans_mv, service_map_*_mv, error_*_mv,
// logs_aggregates_hourly_mv, service_platforms_hourly_mv) still
// pre-extracts ResourceAttributes['deployment.environment'] at write
// time, so omitting it leaves DeploymentEnv='' for every ingest span
// and the services-table badge renders blank.
// Dual-emit the deprecated `deployment.environment` key: the MVs
// coalesce both spellings since migration 0020, but rows materialized
// before it — and pre-0020 BYO-ClickHouse schemas — still read the
// legacy key alone.
.with_attribute(OtelKeyValue::new("deployment.environment", deployment_env))
```

Expand All @@ -58,16 +69,7 @@ Any new service must follow the same priority order. Don't read `NODE_ENV` or `E

### When can the dual-emit be dropped?

When every MV in `packages/domain/src/tinybird/materializations.ts` that currently extracts `ResourceAttributes['deployment.environment']` switches to:

```sql
coalesce(
ResourceAttributes['deployment.environment.name'],
ResourceAttributes['deployment.environment']
) AS DeploymentEnv
```

…and the migration backfills any historical data. Until then, **never remove the legacy emit** — it leaves `DeploymentEnv=''` in the services table and the deployment-env badge renders blank.
ClickHouse migration 0020 moved every MV onto the coalesce, so our own rollups no longer depend on the legacy emit. Two things still do: rows those MVs materialized **before** 0020 (they carry whatever the legacy key held and age out with the target TTL), and BYO-ClickHouse orgs whose schema is still pre-0020. Dropping the legacy emit is a follow-up gated on both, not a free cleanup.

## `maple_org_id` — internal service identity

Expand Down
7 changes: 5 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,11 @@ to per-request hot paths like token validation. OTLP export bypasses the API, so

`apps/ingest` (Rust) self-instruments over OTLP/HTTP to its own `INGEST_FORWARD_OTLP_ENDPOINT`
(startup guard refuses a loopback endpoint), as `service.name="ingest"`, `maple_org_id="internal"`,
and `deployment.environment.name` **dual-emitted** as the legacy `deployment.environment` because
the Tinybird MVs still pre-extract the old key. Custom fields use the `maple.*` namespace. Span
and `deployment.environment.name` **dual-emitted** as the deprecated `deployment.environment`
(the MVs coalesce both since migration 0020 — see `DEPLOYMENT_ENV_SQL` in
`packages/domain/src/tinybird/semconv-renames.ts`, which is where every
renamed-key coalesce belongs; the dual-emit remains for rows already
materialized under the old key and for pre-0020 BYO-ClickHouse schemas). Custom fields use the `maple.*` namespace. Span
status follows OTEL HTTP semconv for SERVER spans: **only 5xx is `Error`, 4xx rejections are `Ok`**
(`otel_status_for_rejection` in `apps/ingest/src/main.rs`) so error dashboards aren't flooded by
expected 401/402/429s. Operational metrics (`apps/ingest/src/metrics.rs`) push via OTLP every 30s —
Expand Down
7 changes: 7 additions & 0 deletions apps/cli/src/server/local-schema-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,11 @@ export const LOCAL_SCHEMA_HISTORY: ReadonlyArray<LocalSchemaHistoryEntry> = Obje
manifestDigest: "f1bdef1ca3dcb073fc3cda998c145f64fd9a4a4f174543e7bc0405867bedf356",
projectRevision: "3773cd0bfa79483773ada07c70c9fa5571688ceecfb1fd5839c33c4251b7f979",
}),
Object.freeze({
version: 10,
fingerprint: "b10c1137fb63a6f3",
digest: "b10c1137fb63a6f3ebd4d44c37ed82d469d1e52ea483c03b3c5cc25399410bd1",
manifestDigest: "7c975d2547ccdedb55f5b5fe2360fa72a4e6b9b7fd1935611e257e207c5694fd",
projectRevision: "5b4c3a0d3aa0962b062689605ad5cf075f47403df04e851ce58133f16fc692e3",
}),
] as const)
2 changes: 1 addition & 1 deletion apps/cli/src/server/local-schema-version.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// 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 = 9 as const
export const LOCAL_SCHEMA_VERSION = 10 as const
2 changes: 2 additions & 0 deletions apps/cli/src/server/local-store-migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { v5ToV6ErrorEventsFingerprintHygieneModule } from "./local-store-migrati
import { v6ToV7ErrorServiceVersionModule } from "./local-store-migrations/v6-to-v7-error-service-version"
import { v7ToV8AppleCrashFramesModule } from "./local-store-migrations/v7-to-v8-apple-crash-frames"
import { v8ToV9MvSweepModule } from "./local-store-migrations/v8-to-v9-mv-sweep"
import { v9ToV10SemconvKeyRenamesModule } from "./local-store-migrations/v9-to-v10-semconv-key-renames"
import type {
AnyLocalStoreMigrationModule,
LocalStoreMigration,
Expand Down Expand Up @@ -111,6 +112,7 @@ export const localStoreMigrations: ReadonlyArray<AnyLocalStoreMigrationModule> =
v6ToV7ErrorServiceVersionModule,
v7ToV8AppleCrashFramesModule,
v8ToV9MvSweepModule,
v9ToV10SemconvKeyRenamesModule,
]

export const validateMigrationRegistry = (
Expand Down
Loading
Loading