Skip to content

Commit d76c5cb

Browse files
authored
Merge pull request #318 from code0-tech/feat/#310
Fix OpenTelemetry setup
2 parents eabe414 + 694c5b3 commit d76c5cb

7 files changed

Lines changed: 175 additions & 83 deletions

File tree

src/app/api/config/route.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import {NextResponse} from "next/server"
2+
import {ClientOtelConfig} from "@core/util/open-telemetry"
3+
4+
// Evaluate on every request so the values reflect the current runtime
5+
// environment rather than being cached from build/first render.
6+
export const dynamic = "force-dynamic"
7+
8+
/**
9+
* Serves the browser OpenTelemetry configuration from server-side environment
10+
* variables at request time. This lets the client telemetry endpoints be
11+
* configured at runtime instead of being inlined into the bundle at build time.
12+
*/
13+
export function GET() {
14+
15+
const config: ClientOtelConfig = {
16+
tracesEndpoint: process.env.NEXT_PUBLIC_OTEL_TRACES_ENDPOINT ?? null,
17+
logsEndpoint: process.env.NEXT_PUBLIC_OTEL_LOGS_ENDPOINT ?? null,
18+
header: process.env.NEXT_PUBLIC_OTEL_HEADER ?? null,
19+
serviceName: process.env.NEXT_PUBLIC_OTEL_SERVICE_NAME ?? null,
20+
environment: process.env.NEXT_PUBLIC_OTEL_ENVIRONMENT ?? null,
21+
version: process.env.NEXT_PUBLIC_SCULPTOR_VERSION ?? "0.0.0",
22+
edition: process.env.NEXT_PUBLIC_EDITION ?? "edition",
23+
}
24+
25+
return NextResponse.json(config, {
26+
headers: {"Cache-Control": "no-store"},
27+
})
28+
}

src/instrumentation-client.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import "@opentelemetry/api-logs"
2-
import initializeLogs, {openTelemetryClientLogsProvider} from "@core/util/open-telemetry-logs"
3-
import initializeTraces, {openTelemetryClientTracesProvider} from "@core/util/open-telemetry-traces"
4-
import {logs} from "@opentelemetry/api-logs"
5-
import {trace} from "@opentelemetry/api"
2+
import {ClientOtelConfig} from "@core/util/open-telemetry"
3+
import {initializeClientLogs} from "@core/util/open-telemetry-logs"
4+
import {initializeClientTraces} from "@core/util/open-telemetry-traces"
65

7-
initializeLogs("client")
8-
if (openTelemetryClientLogsProvider) logs.setGlobalLoggerProvider(openTelemetryClientLogsProvider)
9-
10-
initializeTraces("client")
11-
if (openTelemetryClientTracesProvider) trace.setGlobalTracerProvider(openTelemetryClientTracesProvider)
6+
// The browser OpenTelemetry configuration is fetched from the server at runtime
7+
// (see /api/config) instead of relying on build-time inlined NEXT_PUBLIC_*
8+
// variables, so the collector endpoints can be changed without rebuilding.
9+
fetch("/api/config")
10+
.then((response) => response.json())
11+
.then((config: ClientOtelConfig) => {
12+
initializeClientLogs(config)
13+
initializeClientTraces(config)
14+
})
15+
.catch(() => {
16+
// Telemetry is best-effort; ignore config fetch/initialization failures.
17+
})

src/instrumentation-node.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import {logs} from '@opentelemetry/api-logs'
2+
import {metrics, trace} from "@opentelemetry/api"
3+
import initializeMetrics, {openTelemetryMetricProvider} from "@core/util/open-telemetry-metrics"
4+
import initializeLogs, {openTelemetryServerLogsProvider} from "@core/util/open-telemetry-logs"
5+
import initializeTraces, {openTelemetryServerTracesProvider} from "@core/util/open-telemetry-traces"
6+
7+
// Loaded exclusively from `instrumentation.ts` when NEXT_RUNTIME === "nodejs".
8+
// Keeping the Node OTel SDK behind a dynamic import prevents it from being
9+
// pulled into the Edge Runtime bundle (see instrumentation.ts).
10+
11+
if (openTelemetryMetricProvider) metrics.setGlobalMeterProvider(openTelemetryMetricProvider)
12+
initializeMetrics()
13+
14+
if (openTelemetryServerLogsProvider) logs.setGlobalLoggerProvider(openTelemetryServerLogsProvider)
15+
initializeLogs()
16+
17+
if (openTelemetryServerTracesProvider) trace.setGlobalTracerProvider(openTelemetryServerTracesProvider)
18+
initializeTraces()

src/instrumentation.ts

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,13 @@
1-
import {logs} from '@opentelemetry/api-logs'
2-
import {metrics, trace} from "@opentelemetry/api"
3-
import initializeMetrics, {openTelemetryMetricProvider} from "@core/util/open-telemetry-metrics"
4-
import initializeLogs, {openTelemetryServerLogsProvider} from "@core/util/open-telemetry-logs"
5-
import initializeTraces, {openTelemetryServerTracesProvider} from "@core/util/open-telemetry-traces"
6-
7-
export function register() {
8-
9-
if (openTelemetryMetricProvider) metrics.setGlobalMeterProvider(openTelemetryMetricProvider)
10-
initializeMetrics()
11-
12-
if (openTelemetryServerLogsProvider) logs.setGlobalLoggerProvider(openTelemetryServerLogsProvider)
13-
initializeLogs()
14-
15-
if (openTelemetryServerTracesProvider) trace.setGlobalTracerProvider(openTelemetryServerTracesProvider)
16-
initializeTraces()
1+
export async function register() {
2+
3+
// The OpenTelemetry Node SDK (metrics, logs, traces) relies on Node.js APIs
4+
// such as `process.cpuUsage`, which are not available in the Edge Runtime.
5+
// Next.js runs `register()` for every runtime (including the Edge runtime
6+
// used by `middleware.ts`), so we must only load the Node instrumentation
7+
// when actually running under Node. Using a dynamic import keeps the Node
8+
// SDK out of the Edge bundle entirely.
9+
if (process.env.NEXT_RUNTIME === "nodejs") {
10+
await import("./instrumentation-node")
11+
}
1712

1813
}
19-
20-
21-
22-
23-
24-

src/packages/core/src/util/open-telemetry-logs.ts

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,27 @@
11
import {OTLPLogExporter} from "@opentelemetry/exporter-logs-otlp-http"
22
import {BatchLogRecordProcessor, LoggerProvider} from "@opentelemetry/sdk-logs"
3-
import {resource, serverResource} from "@core/util/open-telemetry"
3+
import {buildClientResource, ClientOtelConfig, serverResource} from "@core/util/open-telemetry"
44
import {recordException} from "@core/util/open-telemetry-exceptions"
5+
import {logs} from "@opentelemetry/api-logs"
56
import {parseHeaders} from "@core/util/headers";
67

78
export const openTelemetryServerLogsReader = process.env.OTEL_LOGS_ENDPOINT ? new OTLPLogExporter({
89
url: process.env.OTEL_LOGS_ENDPOINT,
910
headers: parseHeaders(process.env.OTEL_HEADER),
1011
}) : undefined
1112

12-
export const openTelemetryClientLogsReader = process.env.NEXT_PUBLIC_OTEL_LOGS_ENDPOINT ? new OTLPLogExporter({
13-
url: process.env.NEXT_PUBLIC_OTEL_LOGS_ENDPOINT,
14-
headers: parseHeaders(process.env.NEXT_PUBLIC_OTEL_HEADER),
15-
}) : undefined
16-
1713
export const openTelemetryServerLogsProvider = openTelemetryServerLogsReader ? new LoggerProvider({
1814
resource: serverResource,
1915
processors: [new BatchLogRecordProcessor(openTelemetryServerLogsReader)]
2016
}) : undefined
2117

22-
export const openTelemetryClientLogsProvider = openTelemetryClientLogsReader ? new LoggerProvider({
23-
resource: resource,
24-
processors: [new BatchLogRecordProcessor(openTelemetryClientLogsReader)]
25-
}) : undefined
26-
27-
export default (level: 'server' | "client" = "server") => {
18+
/**
19+
* Patches the global `console` methods to forward messages to the given
20+
* OpenTelemetry logger provider (in addition to the original console output).
21+
*/
22+
const instrumentConsole = (provider: LoggerProvider) => {
2823

29-
if (level === 'server' && !openTelemetryServerLogsProvider) return
30-
if (level === 'client' && !openTelemetryClientLogsProvider) return
31-
32-
const logger = level === "server" ?
33-
openTelemetryServerLogsProvider?.getLogger("default", "1.0.0") :
34-
openTelemetryClientLogsProvider?.getLogger("default", "1.0.0")
24+
const logger = provider.getLogger("default", "1.0.0")
3525

3626
const originalConsole = {
3727
log: console.log,
@@ -147,4 +137,35 @@ export default (level: 'server' | "client" = "server") => {
147137
originalConsole.debug.apply(console, args)
148138
}
149139

150-
}
140+
}
141+
142+
export default () => {
143+
144+
if (!openTelemetryServerLogsProvider) return
145+
instrumentConsole(openTelemetryServerLogsProvider)
146+
}
147+
148+
/**
149+
* Builds and registers the browser logger provider from runtime configuration,
150+
* then patches `console` to forward log records to it. Returns the provider, or
151+
* undefined when logging is not configured.
152+
*/
153+
export const initializeClientLogs = (config: ClientOtelConfig) => {
154+
155+
if (!config.logsEndpoint) return undefined
156+
157+
const exporter = new OTLPLogExporter({
158+
url: config.logsEndpoint,
159+
headers: parseHeaders(config.header ?? undefined),
160+
})
161+
162+
const provider = new LoggerProvider({
163+
resource: buildClientResource(config),
164+
processors: [new BatchLogRecordProcessor(exporter)]
165+
})
166+
167+
logs.setGlobalLoggerProvider(provider)
168+
instrumentConsole(provider)
169+
170+
return provider
171+
}

src/packages/core/src/util/open-telemetry-traces.ts

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,53 +2,67 @@ import {OTLPTraceExporter} from '@opentelemetry/exporter-trace-otlp-http'
22
import {WebTracerProvider} from '@opentelemetry/sdk-trace-web'
33
import {BatchSpanProcessor} from '@opentelemetry/sdk-trace-base'
44
import {XMLHttpRequestInstrumentation} from '@opentelemetry/instrumentation-xml-http-request'
5-
import {resource, serverResource} from "@core/util/open-telemetry"
5+
import {buildClientResource, ClientOtelConfig, serverResource} from "@core/util/open-telemetry"
66
import {registerInstrumentations} from '@opentelemetry/instrumentation'
77
import {FetchInstrumentation} from '@opentelemetry/instrumentation-fetch'
88
import {ZoneContextManager} from '@opentelemetry/context-zone'
9+
import {trace} from "@opentelemetry/api"
910
import {parseHeaders} from "@core/util/headers";
1011

1112
export const openTelemetryServerTracesReader = process.env.OTEL_TRACES_ENDPOINT ? new OTLPTraceExporter({
1213
url: process.env.OTEL_TRACES_ENDPOINT,
1314
headers: parseHeaders(process.env.OTEL_HEADER),
1415
}) : undefined
1516

16-
export const openTelemetryClientTracesReader = process.env.NEXT_PUBLIC_OTEL_TRACES_ENDPOINT ? new OTLPTraceExporter({
17-
url: process.env.NEXT_PUBLIC_OTEL_TRACES_ENDPOINT || "",
18-
headers: parseHeaders(process.env.NEXT_PUBLIC_OTEL_HEADER),
19-
}) : undefined
20-
2117
export const openTelemetryServerTracesProvider = openTelemetryServerTracesReader ? new WebTracerProvider({
2218
resource: serverResource,
2319
spanProcessors: [new BatchSpanProcessor(openTelemetryServerTracesReader)],
2420
}) : undefined
2521

26-
export const openTelemetryClientTracesProvider = openTelemetryClientTracesReader ? new WebTracerProvider({
27-
resource: resource,
28-
spanProcessors: [new BatchSpanProcessor(openTelemetryClientTracesReader)],
29-
}) : undefined
30-
31-
export default (level: 'server' | "client" = "server") => {
22+
export default () => {
3223

33-
if (level === 'server' && openTelemetryServerTracesProvider) {
24+
if (openTelemetryServerTracesProvider) {
3425
openTelemetryServerTracesProvider.register({
3526
contextManager: new ZoneContextManager(),
3627
})
37-
} else if (level === 'client' && openTelemetryClientTracesProvider) {
38-
openTelemetryClientTracesProvider.register({
39-
contextManager: new ZoneContextManager(),
40-
})
41-
42-
registerInstrumentations({
43-
instrumentations: [
44-
new FetchInstrumentation({
45-
propagateTraceHeaderCorsUrls: /.*/,
46-
}),
47-
new XMLHttpRequestInstrumentation({
48-
propagateTraceHeaderCorsUrls: /.*/,
49-
}),
50-
],
51-
})
5228
}
29+
}
30+
31+
/**
32+
* Builds and registers the browser trace provider from runtime configuration.
33+
* Returns the provider so the caller can install it as the global tracer
34+
* provider, or undefined when tracing is not configured.
35+
*/
36+
export const initializeClientTraces = (config: ClientOtelConfig) => {
37+
38+
if (!config.tracesEndpoint) return undefined
39+
40+
const exporter = new OTLPTraceExporter({
41+
url: config.tracesEndpoint,
42+
headers: parseHeaders(config.header ?? undefined),
43+
})
44+
45+
const provider = new WebTracerProvider({
46+
resource: buildClientResource(config),
47+
spanProcessors: [new BatchSpanProcessor(exporter)],
48+
})
49+
50+
provider.register({
51+
contextManager: new ZoneContextManager(),
52+
})
53+
54+
registerInstrumentations({
55+
instrumentations: [
56+
new FetchInstrumentation({
57+
propagateTraceHeaderCorsUrls: /.*/,
58+
}),
59+
new XMLHttpRequestInstrumentation({
60+
propagateTraceHeaderCorsUrls: /.*/,
61+
}),
62+
],
63+
})
64+
65+
trace.setGlobalTracerProvider(provider)
5366

54-
}
67+
return provider
68+
}

src/packages/core/src/util/open-telemetry.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,25 @@
11
import {resourceFromAttributes} from "@opentelemetry/resources";
22

3-
export const resource = resourceFromAttributes({
4-
...(process.env.NEXT_PUBLIC_OTEL_SERVICE_NAME ? {'service.name': process.env.NEXT_PUBLIC_OTEL_SERVICE_NAME} : {}),
5-
'service.version': (process.env.NEXT_PUBLIC_SCULPTOR_VERSION ?? "0.0.0") + "-" + (process.env.NEXT_PUBLIC_EDITION ?? "edition"),
6-
...(process.env.NEXT_PUBLIC_OTEL_ENVIRONMENT ? {'deployment.environment.name': process.env.NEXT_PUBLIC_OTEL_ENVIRONMENT} : {}),
3+
/**
4+
* Runtime OpenTelemetry configuration for the browser. These values are served
5+
* by the `/api/config` route handler and read on the server at request
6+
* time, so they can be changed at runtime without rebuilding the app (unlike
7+
* `NEXT_PUBLIC_*` variables, which are inlined at build time).
8+
*/
9+
export interface ClientOtelConfig {
10+
tracesEndpoint?: string | null
11+
logsEndpoint?: string | null
12+
header?: string | null
13+
serviceName?: string | null
14+
environment?: string | null
15+
version?: string | null
16+
edition?: string | null
17+
}
18+
19+
export const buildClientResource = (config: ClientOtelConfig) => resourceFromAttributes({
20+
...(config.serviceName ? {'service.name': config.serviceName} : {}),
21+
'service.version': (config.version ?? "0.0.0") + "-" + (config.edition ?? "edition"),
22+
...(config.environment ? {'deployment.environment.name': config.environment} : {}),
723
});
824

925
export const serverResource = resourceFromAttributes({

0 commit comments

Comments
 (0)