mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-04 00:14:12 +00:00
With `OTEL_EXPORTER_OTLP_ENDPOINT` or `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` set (and neither `OTEL_SDK_DISABLED=true` nor `OTEL_TRACES_EXPORTER=none`), Bulwark loads the OpenTelemetry SDK at startup and exports Next's own spans plus a client span for every call to Stalwart, WOPI, translation, the plugin proxy and OAuth over OTLP/HTTP. Calls to Stalwart carry a `traceparent` header, so Stalwart's spans join the trace. All other settings come from the standard `OTEL_*` variables. Every span is scrubbed before export: URLs keep their origin only, request paths become their route group, query strings, user agents and client addresses are dropped, and addresses or credentials in other attributes are removed. Without an endpoint the SDK is never loaded. The endpoint is listed under the hosts that stay reachable with no third-party traffic, and System > Monitoring shows whether tracing runs.
63 lines
2.4 KiB
TypeScript
63 lines
2.4 KiB
TypeScript
import { observeUpstreamResult, type UpstreamTarget } from './index';
|
|
import { isTracingActive } from '@/lib/tracing/config';
|
|
|
|
/** Headers that carry the trace context to the service called (`traceparent`). */
|
|
export type TraceHeaders = Record<string, string>;
|
|
|
|
/**
|
|
* Run one request this server makes to another service, under a fixed
|
|
* target name:
|
|
*
|
|
* - metrics (when /api/metrics is on): duration and outcome. An answer
|
|
* below 500 counts as ok (a 401 from Stalwart is an answer); a 5xx or a
|
|
* thrown error as error.
|
|
* - tracing (when OTEL_* is set): a client span named after the target,
|
|
* with no URL, host or user in it, and the W3C trace context handed to
|
|
* `run` as headers, so the callee's spans join the trace.
|
|
*
|
|
* Costs nothing while both are off.
|
|
*/
|
|
export async function observeUpstream<T extends { status: number }>(
|
|
target: UpstreamTarget,
|
|
run: (traceHeaders: TraceHeaders) => Promise<T>,
|
|
): Promise<T> {
|
|
if (isTracingActive()) return traced(target, run);
|
|
return measured(target, () => run({}));
|
|
}
|
|
|
|
async function measured<T extends { status: number }>(target: UpstreamTarget, run: () => Promise<T>): Promise<T> {
|
|
const start = performance.now();
|
|
try {
|
|
const response = await run();
|
|
observeUpstreamResult(target, response.status < 500, (performance.now() - start) / 1000);
|
|
return response;
|
|
} catch (error) {
|
|
observeUpstreamResult(target, false, (performance.now() - start) / 1000);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function traced<T extends { status: number }>(target: UpstreamTarget, run: (traceHeaders: TraceHeaders) => Promise<T>): Promise<T> {
|
|
const api = await import('@opentelemetry/api');
|
|
const tracer = api.trace.getTracer('bulwark-webmail');
|
|
return tracer.startActiveSpan(`upstream ${target}`, {
|
|
kind: api.SpanKind.CLIENT,
|
|
attributes: { 'bulwark.upstream.target': target },
|
|
}, async (span) => {
|
|
const headers: TraceHeaders = {};
|
|
api.propagation.inject(api.context.active(), headers);
|
|
try {
|
|
const response = await measured(target, () => run(headers));
|
|
span.setAttribute('http.response.status_code', response.status);
|
|
if (response.status >= 500) span.setStatus({ code: api.SpanStatusCode.ERROR });
|
|
return response;
|
|
} catch (error) {
|
|
// The error's name only: messages can name hosts or paths.
|
|
span.setStatus({ code: api.SpanStatusCode.ERROR, message: error instanceof Error ? error.name : 'Error' });
|
|
throw error;
|
|
} finally {
|
|
span.end();
|
|
}
|
|
});
|
|
}
|