UNPKG

@upstash/context7-mcp

Version:
243 lines (242 loc) • 9.88 kB
import { metrics, trace } from "@opentelemetry/api"; import { markCurrentMcpOperationError, markCurrentMcpToolOutcome } from "./mcp-operation-scope.js"; const METER_NAME = "io.github.upstash.context7.mcp"; const DURATION_BUCKETS_SECONDS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60]; const TIMEOUT_ERROR_CODES = new Set([ "ETIMEDOUT", "UND_ERR_BODY_TIMEOUT", "UND_ERR_CONNECT_TIMEOUT", "UND_ERR_HEADERS_TIMEOUT", ]); const NETWORK_ERROR_CODES = new Set([ "EAI_AGAIN", "ECONNREFUSED", "ECONNRESET", "EHOSTUNREACH", "ENETUNREACH", "ENOTFOUND", "EPIPE", ]); function createInstruments() { const meter = metrics.getMeter(METER_NAME); return { upstreamRequests: meter.createCounter("context7.mcp.upstream.requests", { description: "Number of requests made to Context7 dependencies", unit: "{request}", }), upstreamRequestDuration: meter.createHistogram("context7.mcp.upstream.request.duration", { description: "Duration of requests made to Context7 dependencies", unit: "s", advice: { explicitBucketBoundaries: DURATION_BUCKETS_SECONDS }, }), activeUpstreamRequests: meter.createUpDownCounter("context7.mcp.upstream.requests.active", { description: "Number of requests to Context7 dependencies currently in flight", unit: "{request}", }), authenticationAttempts: meter.createCounter("context7.mcp.authentication.attempts", { description: "Number of hosted MCP authentication decisions", unit: "{attempt}", }), authenticationEvents: meter.createCounter("context7.mcp.authentication.events", { description: "Number of hosted MCP authentication lifecycle events", unit: "{event}", }), selfCancellingBatches: meter.createCounter("context7.mcp.batches.self_cancelling", { description: "JSON-RPC batches in which a request was cancelled by the same batch", unit: "{batch}", }), authenticationDuration: meter.createHistogram("context7.mcp.authentication.duration", { description: "Duration of hosted MCP authentication decisions", unit: "s", advice: { explicitBucketBoundaries: DURATION_BUCKETS_SECONDS }, }), activeAuthentications: meter.createUpDownCounter("context7.mcp.authentication.active", { description: "Number of hosted MCP requests currently authenticating", unit: "{request}", }), }; } let instruments; function getInstruments() { instruments ??= createInstruments(); return instruments; } function elapsedSeconds(startedAt) { return (performance.now() - startedAt) / 1_000; } function statusClass(statusCode) { if (statusCode >= 100 && statusCode <= 599) { return `${Math.floor(statusCode / 100)}xx`; } return "unknown"; } export function classifyUpstreamError(error, abortSignal, fallback = "network_error") { let timeout = false; let cancelled = false; let networkError = false; const rootCount = abortSignal?.aborted ? 2 : 1; for (let rootIndex = 0; rootIndex < rootCount; rootIndex += 1) { let value = rootIndex === 0 ? error : abortSignal?.reason; for (let depth = 0; value && typeof value === "object" && depth < 8; depth += 1) { const current = value; const name = current.name; const code = current.code; if (name === "TimeoutError" || (typeof code === "string" && TIMEOUT_ERROR_CODES.has(code))) { timeout = true; } else if (name === "AbortError" || code === "ABORT_ERR") { cancelled = true; } else if (typeof code === "string" && (code.startsWith("UND_ERR_") || NETWORK_ERROR_CODES.has(code))) { networkError = true; } value = current.cause; } } if (timeout) return "timeout"; if (cancelled || abortSignal?.aborted) return "cancelled"; if (networkError) return "network_error"; return fallback; } export function recordToolCallOutcome(outcome) { if (outcome === "error") markCurrentMcpOperationError(); markCurrentMcpToolOutcome(outcome); } export async function observeUpstreamRequest(operationName, request, consumeResponse, options = {}) { const { activeUpstreamRequests, upstreamRequestDuration, upstreamRequests } = getInstruments(); const activeAttributes = { "context7.upstream.operation": operationName }; const startedAt = performance.now(); let outcome = "network_error"; let responseStatus; let responseStatusClass = "none"; activeUpstreamRequests.add(1, activeAttributes); try { let response; try { response = await request(); } catch (error) { outcome = classifyUpstreamError(error, options.abortSignal); throw error; } responseStatus = response.status; responseStatusClass = statusClass(response.status); outcome = response.ok ? "success" : "http_error"; try { return await consumeResponse(response); } catch (error) { outcome = classifyUpstreamError(error, options.abortSignal, "response_error"); throw error; } } finally { const attributes = { ...activeAttributes, "http.response.status_code_class": responseStatusClass, "context7.upstream.outcome": outcome, }; if (responseStatus !== undefined) { attributes["http.response.status_code"] = responseStatus; } activeUpstreamRequests.add(-1, activeAttributes); upstreamRequests.add(1, attributes); upstreamRequestDuration.record(elapsedSeconds(startedAt), attributes); } } function flushableProvider(provider) { let current = provider; for (let depth = 0; current && typeof current === "object" && depth < 4; depth += 1) { if ("forceFlush" in current && typeof current.forceFlush === "function") { return current; } if (!("getDelegate" in current) || typeof current.getDelegate !== "function") return undefined; const delegate = current.getDelegate(); if (delegate === current) return undefined; current = delegate; } return undefined; } export async function forceFlushTelemetry() { const providers = [ flushableProvider(metrics.getMeterProvider()), flushableProvider(trace.getTracerProvider()), ].filter((provider) => provider !== undefined); const results = await Promise.allSettled(providers.map((provider) => provider.forceFlush.call(provider))); const failures = results .filter((result) => result.status === "rejected") .map((result) => result.reason); if (failures.length > 0) { throw new AggregateError(failures, "OpenTelemetry providers failed to flush during shutdown"); } } export async function observeAuthentication(options, operation) { const { activeAuthentications, authenticationAttempts, authenticationDuration, authenticationEvents, } = getInstruments(); const activeAttributes = { "context7.mcp.route": options.route }; const startedAt = performance.now(); let outcome = "error"; activeAuthentications.add(1, activeAttributes); try { const observed = await operation(); outcome = observed.outcome; const attributes = { ...activeAttributes, "context7.authentication.enforcement": options.enforcementMode, "context7.authentication.event": observed.event, "context7.authentication.method": observed.method, "context7.authentication.outcome": outcome, }; authenticationEvents.add(1, attributes); return observed; } finally { const attributes = { ...activeAttributes, "context7.authentication.enforcement": options.enforcementMode, "context7.authentication.outcome": outcome, }; activeAuthentications.add(-1, activeAttributes); authenticationAttempts.add(1, attributes); authenticationDuration.record(elapsedSeconds(startedAt), attributes); } } const MAX_BATCH_CLIENT_NAMES = 50; const batchClientNames = new Set(); /** * Counts a batch that cancelled its own request, by the product token of the * caller's User-Agent. The header is caller-controlled, so the label set is * capped per process; later names are counted as "other". */ export function recordSelfCancellingBatch(userAgent) { let client = /^[\w.-]{1,40}/.exec(userAgent ?? "")?.[0].toLowerCase() ?? "unknown"; if (!batchClientNames.has(client)) { if (batchClientNames.size >= MAX_BATCH_CLIENT_NAMES) client = "other"; else batchClientNames.add(client); } getInstruments().selfCancellingBatches.add(1, { "user_agent.name": client }); } export function recordAuthenticationEvent(observation) { const attributes = { "context7.authentication.enforcement": observation.enforcementMode, "context7.authentication.event": observation.event, "context7.authentication.method": observation.method, "context7.mcp.route": observation.route, }; if (observation.outcome) { attributes["context7.authentication.outcome"] = observation.outcome; } if (observation.oauthClientRegistration) { attributes["context7.authentication.oauth_client_registration"] = observation.oauthClientRegistration; } getInstruments().authenticationEvents.add(1, attributes); }