@accounter/server
Version:
Accounter GraphQL server
123 lines (111 loc) • 3.76 kB
text/typescript
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { NodeSDK, resources, tracing } from '@opentelemetry/sdk-node';
import { ATTR_SERVICE_NAME, ATTR_SERVICE_NAMESPACE } from '@opentelemetry/semantic-conventions';
import { env } from '../environment.js';
const ATTR_DEPLOYMENT_ENVIRONMENT_NAME = 'deployment.environment.name';
const HEALTH_CHECK_PATHS = new Set(['/health', '/ready', '/readiness']);
function shouldIgnoreIncomingRequest(url: string | undefined): boolean {
if (!url) {
return false;
}
try {
const pathname = new URL(url, 'http://localhost').pathname;
return HEALTH_CHECK_PATHS.has(pathname);
} catch {
const pathname = url.split('?')[0] ?? '';
return HEALTH_CHECK_PATHS.has(pathname);
}
}
/**
* Parses an OTLP header string of the form "key1=value1,key2=value2" into a
* Record<string, string>. Returns undefined when the input is absent or empty.
*/
function parseHeaders(headersStr: string | undefined): Record<string, string> | undefined {
if (!headersStr) return undefined;
const result: Record<string, string> = {};
for (const part of headersStr.split(',')) {
const eqIdx = part.indexOf('=');
if (eqIdx === -1) continue;
const key = part.slice(0, eqIdx).trim();
const value = part.slice(eqIdx + 1).trim();
if (key) result[key] = value;
}
return Object.keys(result).length > 0 ? result : undefined;
}
function buildSampler(
samplerType:
| 'always_on'
| 'always_off'
| 'parentbased_traceidratio'
| 'traceidratio'
| 'parentbased_always_on'
| 'parentbased_always_off',
arg: number | undefined,
) {
switch (samplerType) {
case 'always_off':
return new tracing.AlwaysOffSampler();
case 'parentbased_traceidratio': {
const ratio = arg ?? 1.0;
return new tracing.ParentBasedSampler({
root: new tracing.TraceIdRatioBasedSampler(ratio),
});
}
case 'traceidratio': {
const ratio = arg ?? 1.0;
return new tracing.TraceIdRatioBasedSampler(ratio);
}
case 'parentbased_always_on':
return new tracing.ParentBasedSampler({
root: new tracing.AlwaysOnSampler(),
});
case 'parentbased_always_off':
return new tracing.ParentBasedSampler({
root: new tracing.AlwaysOffSampler(),
});
default:
return new tracing.AlwaysOnSampler();
}
}
/**
* Builds and returns a configured (but not yet started) NodeSDK instance using
* the OTEL settings from `env`. Returns null when OTEL is disabled.
*/
export function buildOtelSdk(): NodeSDK | null {
const otel = env.otel;
if (!otel.enabled) {
return null;
}
const resource = resources.resourceFromAttributes({
[ATTR_SERVICE_NAME]: otel.serviceName,
[ATTR_SERVICE_NAMESPACE]: otel.serviceNamespace,
[ATTR_DEPLOYMENT_ENVIRONMENT_NAME]: otel.deploymentEnv,
});
const traceExporter = new OTLPTraceExporter({
url: otel.exporterEndpoint,
headers: parseHeaders(otel.exporterHeaders),
});
const sampler = buildSampler(otel.tracesSampler, otel.tracesSamplerArg);
return new NodeSDK({
resource,
traceExporter,
sampler,
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-fs': {
enabled: false,
},
'@opentelemetry/instrumentation-http': {
ignoreIncomingRequestHook: request => shouldIgnoreIncomingRequest(request.url),
},
'@opentelemetry/instrumentation-graphql': {
enabled: true,
},
'@opentelemetry/instrumentation-pg': {
enhancedDatabaseReporting: false,
},
}),
],
});
}