Skip to content

Commit ed39edb

Browse files
feat(observability): export Trigger.dev telemetry to Grafana Cloud OTLP (#4583)
* feat(observability): export Trigger.dev telemetry to Grafana Cloud OTLP Wire OTLP HTTP exporters for traces, logs, and metrics from the Trigger.dev runtime to Grafana Cloud. Auth uses Basic with instance ID and API token. Gated behind GRAFANA_OTLP_ENDPOINT, GRAFANA_INSTANCE_ID, and GRAFANA_API_TOKEN — all three must be set together or all unset; partial config throws at startup. * improvement(observability): use OTLP HTTP/JSON for metrics for consistency with traces and logs * feat(observability): tag Trigger.dev telemetry with deployment.environment.name * improvement(observability): switch Grafana telemetry vars to OTLP-shaped trio
1 parent 2441d5a commit ed39edb

4 files changed

Lines changed: 65 additions & 0 deletions

File tree

apps/sim/lib/core/config/env.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,9 @@ export const env = createEnv({
157157
LOG_LEVEL: z.enum(['DEBUG', 'INFO', 'WARN', 'ERROR']).optional(), // Minimum log level to display (defaults to ERROR in production, DEBUG in development)
158158
PROFOUND_API_KEY: z.string().min(1).optional(), // Profound analytics API key
159159
PROFOUND_ENDPOINT: z.string().url().optional(), // Profound analytics endpoint
160+
GRAFANA_OTLP_ENDPOINT: z.string().url().optional(), // Grafana Cloud OTLP HTTP gateway base URL (e.g., https://otlp-gateway-prod-us-east-0.grafana.net/otlp). Trigger.dev exporters append /v1/traces, /v1/logs, /v1/metrics.
161+
GRAFANA_OTLP_HEADERS: z.string().min(1).optional(), // Comma-separated key=value headers for OTLP requests (e.g., "Authorization=Basic <base64(instanceId:token)>"). Same format as the OTEL_EXPORTER_OTLP_HEADERS spec.
162+
GRAFANA_DEPLOYMENT_ENVIRONMENT: z.string().min(1).optional(), // Deployment tier label (e.g., "production", "staging", "development"). Emitted as the stable `deployment.environment.name` resource attribute on Trigger.dev telemetry to match the rest of the Sim OTEL stack.
160163

161164
// External Services
162165
BROWSERBASE_API_KEY: z.string().min(1).optional(), // Browserbase API key for browser automation

apps/sim/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@
6565
"@modelcontextprotocol/sdk": "1.29.0",
6666
"@monaco-editor/react": "4.7.0",
6767
"@opentelemetry/api": "^1.9.0",
68+
"@opentelemetry/exporter-logs-otlp-http": "^0.217.0",
69+
"@opentelemetry/exporter-metrics-otlp-http": "^0.217.0",
6870
"@opentelemetry/exporter-trace-otlp-http": "^0.217.0",
6971
"@opentelemetry/resources": "^2.7.0",
7072
"@opentelemetry/sdk-node": "^0.217.0",

apps/sim/trigger.config.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,64 @@
1+
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http'
2+
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'
3+
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
4+
import { resourceFromAttributes } from '@opentelemetry/resources'
15
import { additionalFiles, additionalPackages } from '@trigger.dev/build/extensions/core'
26
import { defineConfig } from '@trigger.dev/sdk'
37
import { env } from './lib/core/config/env'
48

9+
const grafanaEndpoint = env.GRAFANA_OTLP_ENDPOINT
10+
const grafanaHeaders = env.GRAFANA_OTLP_HEADERS
11+
const grafanaDeploymentEnvironment = env.GRAFANA_DEPLOYMENT_ENVIRONMENT
12+
const grafanaConfigured = Boolean(grafanaEndpoint || grafanaHeaders || grafanaDeploymentEnvironment)
13+
const grafanaFullyConfigured = Boolean(
14+
grafanaEndpoint && grafanaHeaders && grafanaDeploymentEnvironment
15+
)
16+
17+
if (grafanaConfigured && !grafanaFullyConfigured) {
18+
throw new Error(
19+
'Grafana OTLP telemetry is partially configured. Set GRAFANA_OTLP_ENDPOINT, GRAFANA_OTLP_HEADERS, and GRAFANA_DEPLOYMENT_ENVIRONMENT together, or leave all three unset.'
20+
)
21+
}
22+
23+
/**
24+
* Parse OTLP headers per the OTEL spec format `key1=value1,key2=value2`.
25+
* Values are URL-decoded; keys/values are trimmed; empty entries are skipped.
26+
* @see https://opentelemetry.io/docs/specs/otel/protocol/exporter/
27+
*/
28+
function parseOtlpHeaders(raw: string): Record<string, string> {
29+
const out: Record<string, string> = {}
30+
for (const pair of raw.split(',')) {
31+
const eq = pair.indexOf('=')
32+
if (eq === -1) continue
33+
const key = pair.slice(0, eq).trim()
34+
const value = pair.slice(eq + 1).trim()
35+
if (!key) continue
36+
out[key] = decodeURIComponent(value)
37+
}
38+
return out
39+
}
40+
41+
const grafanaTelemetry = grafanaFullyConfigured
42+
? (() => {
43+
const baseUrl = grafanaEndpoint!.replace(/\/+$/, '')
44+
const headers = parseOtlpHeaders(grafanaHeaders!)
45+
if (Object.keys(headers).length === 0) {
46+
throw new Error(
47+
'GRAFANA_OTLP_HEADERS is set but yielded no valid key=value pairs. Expected format: "key1=value1,key2=value2".'
48+
)
49+
}
50+
const resource = resourceFromAttributes({
51+
'deployment.environment.name': grafanaDeploymentEnvironment!,
52+
})
53+
return {
54+
exporters: [new OTLPTraceExporter({ url: `${baseUrl}/v1/traces`, headers })],
55+
logExporters: [new OTLPLogExporter({ url: `${baseUrl}/v1/logs`, headers })],
56+
metricExporters: [new OTLPMetricExporter({ url: `${baseUrl}/v1/metrics`, headers })],
57+
resource,
58+
}
59+
})()
60+
: undefined
61+
562
export default defineConfig({
663
project: env.TRIGGER_PROJECT_ID!,
764
runtime: 'node-22',
@@ -14,6 +71,7 @@ export default defineConfig({
1471
},
1572
},
1673
dirs: ['./background'],
74+
...(grafanaTelemetry ? { telemetry: grafanaTelemetry } : {}),
1775
build: {
1876
external: ['isolated-vm'],
1977
extensions: [

bun.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)