Skip to main content
Glama
instrumentation.ts5.29 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { BullMQInstrumentation } from '@appsignal/opentelemetry-instrumentation-bullmq'; import { MEDPLUM_VERSION } from '@medplum/core'; import type { Span } from '@opentelemetry/api'; import { diag, DiagConsoleLogger, DiagLogLevel, SpanStatusCode } from '@opentelemetry/api'; import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto'; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'; import { DataloaderInstrumentation } from '@opentelemetry/instrumentation-dataloader'; import { ExpressInstrumentation, ExpressLayerType } from '@opentelemetry/instrumentation-express'; import { GraphQLInstrumentation } from '@opentelemetry/instrumentation-graphql'; import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; import { IORedisInstrumentation } from '@opentelemetry/instrumentation-ioredis'; import type { PgResponseHookInformation } from '@opentelemetry/instrumentation-pg'; import { PgInstrumentation } from '@opentelemetry/instrumentation-pg'; import { RuntimeNodeInstrumentation } from '@opentelemetry/instrumentation-runtime-node'; import { defaultResource, resourceFromAttributes } from '@opentelemetry/resources'; import type { MetricReader } from '@opentelemetry/sdk-metrics'; import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'; import { NodeSDK } from '@opentelemetry/sdk-node'; import type { SpanExporter } from '@opentelemetry/sdk-trace-base'; import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions'; import type { ClientRequest, IncomingMessage, ServerResponse } from 'node:http'; // This file includes OpenTelemetry instrumentation. // Note that this file is related but separate from the OpenTelemetry helpers in otel.ts. // This file is used to initialize OpenTelemetry, and must be loaded before any other code. // // References: // https://opentelemetry.io/docs/instrumentation/js/getting-started/nodejs/ // https://opentelemetry.io/docs/instrumentation/js/exporters/ diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO); let sdk: NodeSDK | undefined = undefined; export function initOpenTelemetry(): void { const OTLP_METRICS_ENDPOINT = process.env.OTLP_METRICS_ENDPOINT; const OTLP_TRACES_ENDPOINT = process.env.OTLP_TRACE_ENDPOINT; if (!OTLP_METRICS_ENDPOINT && !OTLP_TRACES_ENDPOINT) { return; } const resource = defaultResource().merge( resourceFromAttributes({ [ATTR_SERVICE_NAME]: 'medplum', [ATTR_SERVICE_VERSION]: MEDPLUM_VERSION, }) ); let metricReader: MetricReader | undefined = undefined; if (OTLP_METRICS_ENDPOINT) { const exporter = new OTLPMetricExporter({ url: OTLP_METRICS_ENDPOINT }); metricReader = new PeriodicExportingMetricReader({ exporter }); } let traceExporter: SpanExporter | undefined = undefined; if (OTLP_TRACES_ENDPOINT) { traceExporter = new OTLPTraceExporter({ url: OTLP_TRACES_ENDPOINT }); } const instrumentations = [ new RuntimeNodeInstrumentation(), new HttpInstrumentation({ applyCustomAttributesOnSpan: httpResponseHook, }), new PgInstrumentation({ enhancedDatabaseReporting: true, requireParentSpan: true, responseHook: pgResponseHook, }), new IORedisInstrumentation(), new ExpressInstrumentation({ // In order to reduce the number of spans in traces sent to the backend, we omit // some common middleware that don't contribute interesting information to the // request timeline. These generally take ~zero time to run and don't fail specifically ignoreLayers: [ 'expressInit', 'query', 'urlencodedParser', 'textParser', 'setupResponseInterceptors', 'standardHeaders', 'corsMiddleware', 'compression', ].map((name) => `middleware - ${name}`), ignoreLayersType: [ExpressLayerType.ROUTER], }), new GraphQLInstrumentation({ ignoreTrivialResolveSpans: true, // Don't record simple object property lookups }), new DataloaderInstrumentation(), new BullMQInstrumentation({ requireParentSpanForPublish: true, }), ]; sdk = new NodeSDK({ resource, instrumentations, metricReaders: metricReader ? [metricReader] : undefined, traceExporter, }); sdk.start(); } export function httpResponseHook( span: Span, req: IncomingMessage | ClientRequest, res: ServerResponse | IncomingMessage ): void { // All error traces are kept, but others may be sampled const code = res.statusCode && res.statusCode < 500 ? SpanStatusCode.OK : SpanStatusCode.ERROR; span.setStatus({ code }); span.setAttribute('http.method', req.method ?? 'unknown'); } export function pgResponseHook(span: Span, { data }: PgResponseHookInformation): void { if (data.rowCount !== null) { span.setAttribute('medplum.db.rowCount', data.rowCount); } } export async function shutdownOpenTelemetry(): Promise<void> { if (sdk) { await sdk.shutdown(); sdk = undefined; } } if (process.env.NODE_ENV !== 'test') { // We want to initialize OpenTelemetry only when starting the server initOpenTelemetry(); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/medplum/medplum'

If you have feedback or need assistance with the MCP directory API, please join our Discord server