@graphql-mesh/plugin-opentelemetry
Version:
507 lines (501 loc) • 17.1 kB
JavaScript
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { ZipkinExporter } from '@opentelemetry/exporter-zipkin';
import { ConsoleSpanExporter, BatchSpanProcessor, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { handleMaybePromise } from '@whatwg-node/promise-helpers';
import { getHeadersObj } from '@graphql-mesh/utils';
import { getOperationASTFromDocument, isAsyncIterable, fakePromise } from '@graphql-tools/utils';
import { SpanStatusCode, SpanKind, diag, trace, context, propagation, DiagLogLevel } from '@opentelemetry/api';
import { Resource } from '@opentelemetry/resources';
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import { defaultPrintFn } from '@graphql-mesh/transport-common';
import { SEMATTRS_HTTP_STATUS_CODE, SEMATTRS_HTTP_SCHEME, SEMATTRS_HTTP_ROUTE, SEMATTRS_HTTP_HOST, SEMATTRS_NET_HOST_NAME, SEMATTRS_HTTP_METHOD, SEMATTRS_HTTP_USER_AGENT, SEMATTRS_HTTP_CLIENT_IP, SEMATTRS_HTTP_URL, ATTR_SERVICE_VERSION, ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
function resolveBatchingConfig(exporter, batchingConfig) {
const value = batchingConfig ?? true;
if (value === true) {
return new BatchSpanProcessor(exporter);
} else if (value === false) {
return new SimpleSpanProcessor(exporter);
} else {
return new BatchSpanProcessor(exporter, value);
}
}
function createStdoutExporter(batchingConfig) {
return resolveBatchingConfig(new ConsoleSpanExporter(), batchingConfig);
}
function createZipkinExporter(config, batchingConfig) {
return resolveBatchingConfig(new ZipkinExporter(config), batchingConfig);
}
function createOtlpHttpExporter(config, batchingConfig) {
return resolveBatchingConfig(new OTLPTraceExporter(config), batchingConfig);
}
function loadExporterLazily(exporterName, exporterModuleName, exportNameInModule) {
try {
return handleMaybePromise(
() => import(exporterModuleName),
(mod) => {
const ExportCtor = mod?.default?.[exportNameInModule] || mod?.[exportNameInModule];
if (!ExportCtor) {
throw new Error(
`${exporterName} exporter is not available in the current environment`
);
}
return ExportCtor;
}
);
} catch (err) {
throw new Error(
`${exporterName} exporter is not available in the current environment`
);
}
}
function createOtlpGrpcExporter(config, batchingConfig) {
return handleMaybePromise(
() => loadExporterLazily(
"OTLP gRPC",
"@opentelemetry/exporter-trace-otlp-grpc",
"OTLPTraceExporter"
),
(OTLPTraceExporter) => {
return resolveBatchingConfig(
new OTLPTraceExporter(config),
batchingConfig
);
}
);
}
function createAzureMonitorExporter(config, batchingConfig) {
return handleMaybePromise(
() => loadExporterLazily(
"Azure Monitor",
"@azure/monitor-opentelemetry-exporter",
"AzureMonitorTraceExporter"
),
(AzureMonitorTraceExporter) => {
return resolveBatchingConfig(
new AzureMonitorTraceExporter(config),
batchingConfig
);
}
);
}
const SEMATTRS_GRAPHQL_DOCUMENT = "graphql.document";
const SEMATTRS_GRAPHQL_OPERATION_TYPE = "graphql.operation.type";
const SEMATTRS_GRAPHQL_OPERATION_NAME = "graphql.operation.name";
const SEMATTRS_GRAPHQL_ERROR_COUNT = "graphql.error.count";
const SEMATTRS_GATEWAY_UPSTREAM_SUBGRAPH_NAME = "gateway.upstream.subgraph.name";
function createHttpSpan(input) {
const { url, request, tracer, otelContext } = input;
const path = url.pathname;
const userAgent = request.headers.get("user-agent");
const ips = request.headers.get("x-forwarded-for");
const method = request.method || "GET";
const host = url.host || request.headers.get("host");
const hostname = url.hostname || host || "localhost";
const rootSpanName = `${method} ${path}`;
return tracer.startSpan(
rootSpanName,
{
attributes: {
[SEMATTRS_HTTP_METHOD]: method,
[SEMATTRS_HTTP_URL]: request.url,
[SEMATTRS_HTTP_ROUTE]: path,
[SEMATTRS_HTTP_SCHEME]: url.protocol,
[SEMATTRS_NET_HOST_NAME]: hostname,
[SEMATTRS_HTTP_HOST]: host || void 0,
[SEMATTRS_HTTP_CLIENT_IP]: ips?.split(",")[0],
[SEMATTRS_HTTP_USER_AGENT]: userAgent || void 0
},
kind: SpanKind.SERVER
},
otelContext
);
}
function completeHttpSpan(span, response) {
span.setAttribute(SEMATTRS_HTTP_STATUS_CODE, response.status);
span.setStatus({
code: response.ok ? SpanStatusCode.OK : SpanStatusCode.ERROR,
message: response.ok ? void 0 : response.statusText
});
span.end();
}
function createGraphQLParseSpan(input) {
const parseSpan = input.tracer.startSpan(
"graphql.parse",
{
attributes: {
[SEMATTRS_GRAPHQL_DOCUMENT]: input.query,
[SEMATTRS_GRAPHQL_OPERATION_NAME]: input.operationName
},
kind: SpanKind.INTERNAL
},
input.otelContext
);
return {
parseSpan,
done: (result) => {
if (result instanceof Error) {
parseSpan.setAttribute(SEMATTRS_GRAPHQL_ERROR_COUNT, 1);
parseSpan.recordException(result);
parseSpan.setStatus({
code: SpanStatusCode.ERROR,
message: result.message
});
}
parseSpan.end();
}
};
}
function createGraphQLValidateSpan(input) {
const validateSpan = input.tracer.startSpan(
"graphql.validate",
{
attributes: {
[SEMATTRS_GRAPHQL_DOCUMENT]: input.query,
[SEMATTRS_GRAPHQL_OPERATION_NAME]: input.operationName
},
kind: SpanKind.INTERNAL
},
input.otelContext
);
return {
validateSpan,
done: (result) => {
if (result instanceof Error) {
validateSpan.setStatus({
code: SpanStatusCode.ERROR,
message: result.message
});
} else if (Array.isArray(result) && result.length > 0) {
validateSpan.setAttribute(SEMATTRS_GRAPHQL_ERROR_COUNT, result.length);
validateSpan.setStatus({
code: SpanStatusCode.ERROR,
message: result.map((e) => e.message).join(", ")
});
for (const error in result) {
validateSpan.recordException(error);
}
}
validateSpan.end();
}
};
}
function createGraphQLExecuteSpan(input) {
const operation = getOperationASTFromDocument(
input.args.document,
input.args.operationName || void 0
);
const executeSpan = input.tracer.startSpan(
"graphql.execute",
{
attributes: {
[SEMATTRS_GRAPHQL_OPERATION_TYPE]: operation.operation,
[SEMATTRS_GRAPHQL_OPERATION_NAME]: input.args.operationName || void 0,
[SEMATTRS_GRAPHQL_DOCUMENT]: defaultPrintFn(input.args.document)
},
kind: SpanKind.INTERNAL
},
input.otelContext
);
return {
executeSpan,
done: (result) => {
if (result.errors && result.errors.length > 0) {
executeSpan.setAttribute(
SEMATTRS_GRAPHQL_ERROR_COUNT,
result.errors.length
);
executeSpan.setStatus({
code: SpanStatusCode.ERROR,
message: result.errors.map((e) => e.message).join(", ")
});
for (const error in result.errors) {
executeSpan.recordException(error);
}
}
executeSpan.end();
}
};
}
const subgraphExecReqSpanMap = /* @__PURE__ */ new WeakMap();
function createSubgraphExecuteFetchSpan(input) {
const subgraphExecuteSpan = input.tracer.startSpan(
`subgraph.execute (${input.subgraphName})`,
{
attributes: {
[SEMATTRS_GRAPHQL_OPERATION_NAME]: input.executionRequest.operationName,
[SEMATTRS_GRAPHQL_DOCUMENT]: defaultPrintFn(
input.executionRequest.document
),
[SEMATTRS_GRAPHQL_OPERATION_TYPE]: getOperationASTFromDocument(
input.executionRequest.document,
input.executionRequest.operationName
)?.operation,
[SEMATTRS_GATEWAY_UPSTREAM_SUBGRAPH_NAME]: input.subgraphName
},
kind: SpanKind.CLIENT
},
input.otelContext
);
subgraphExecReqSpanMap.set(input.executionRequest, subgraphExecuteSpan);
return {
done() {
subgraphExecuteSpan.end();
}
};
}
function createUpstreamHttpFetchSpan(input) {
const urlObj = new URL(input.url);
const attributes = {
[SEMATTRS_HTTP_METHOD]: input.fetchOptions.method,
[SEMATTRS_HTTP_URL]: input.url,
[SEMATTRS_NET_HOST_NAME]: urlObj.hostname,
[SEMATTRS_HTTP_HOST]: urlObj.host,
[SEMATTRS_HTTP_ROUTE]: urlObj.pathname,
[SEMATTRS_HTTP_SCHEME]: urlObj.protocol
};
let fetchSpan;
let isOrigSpan;
if (input.executionRequest) {
fetchSpan = subgraphExecReqSpanMap.get(input.executionRequest);
if (fetchSpan) {
isOrigSpan = false;
fetchSpan.setAttributes(attributes);
}
}
if (!fetchSpan) {
fetchSpan = input.tracer.startSpan(
"http.fetch",
{
attributes,
kind: SpanKind.CLIENT
},
input.otelContext
);
isOrigSpan = true;
}
return {
done: (response) => {
fetchSpan.setAttribute(SEMATTRS_HTTP_STATUS_CODE, response.status);
fetchSpan.setStatus({
code: response.ok ? SpanStatusCode.OK : SpanStatusCode.ERROR,
message: response.ok ? void 0 : response.statusText
});
if (isOrigSpan) {
fetchSpan.end();
}
}
};
}
const HeadersTextMapGetter = {
keys(carrier) {
return [...carrier.keys()];
},
get(carrier, key) {
return carrier.get(key) || void 0;
}
};
function useOpenTelemetry(options) {
const inheritContext = options.inheritContext ?? true;
const propagateContext = options.propagateContext ?? true;
const requestContextMapping = /* @__PURE__ */ new WeakMap();
let tracer;
let spanProcessors;
let serviceName = "Gateway";
let provider;
let preparation$;
return {
onYogaInit({ yoga }) {
preparation$ = fakePromise(void 0).then(async () => {
if (!("initializeNodeSDK" in options && options.initializeNodeSDK === false)) {
if (options.serviceName) {
serviceName = options.serviceName;
}
if (options.exporters) {
spanProcessors = await Promise.all(options.exporters);
}
const webProvider = new WebTracerProvider({
resource: new Resource({
[ATTR_SERVICE_NAME]: serviceName,
[ATTR_SERVICE_VERSION]: yoga.version
}),
spanProcessors
});
webProvider.register();
provider = webProvider;
}
const pluginLogger = options.logger.child({ plugin: "OpenTelemetry" });
diag.setLogger(
{
error: (message, ...args) => pluginLogger.error(message, ...args),
warn: (message, ...args) => pluginLogger.warn(message, ...args),
info: (message, ...args) => pluginLogger.info(message, ...args),
debug: (message, ...args) => pluginLogger.debug(message, ...args),
verbose: (message, ...args) => pluginLogger.debug(message, ...args)
},
DiagLogLevel.VERBOSE
);
tracer = options.tracer || trace.getTracer("gateway");
preparation$ = void 0;
});
},
onContextBuilding({ extendContext, context: context2 }) {
extendContext({
opentelemetry: {
tracer,
activeContext: () => requestContextMapping.get(context2.request) ?? context2["active"]()
}
});
},
onRequest(onRequestPayload) {
return handleMaybePromise(
() => preparation$,
() => {
const shouldTraceHttp = typeof options.spans?.http === "function" ? options.spans.http(onRequestPayload) : options.spans?.http ?? true;
if (shouldTraceHttp) {
const { request, url } = onRequestPayload;
const otelContext = inheritContext ? propagation.extract(
context.active(),
request.headers,
HeadersTextMapGetter
) : context.active();
const httpSpan = createHttpSpan({
request,
url,
tracer,
otelContext
});
requestContextMapping.set(
request,
trace.setSpan(otelContext, httpSpan)
);
}
}
);
},
onValidate(onValidatePayload) {
const shouldTraceValidate = typeof options.spans?.graphqlValidate === "function" ? options.spans.graphqlValidate(onValidatePayload) : options.spans?.graphqlValidate ?? true;
const { context: context2 } = onValidatePayload;
const otelContext = requestContextMapping.get(context2.request);
if (shouldTraceValidate && otelContext) {
const { done } = createGraphQLValidateSpan({
otelContext,
tracer,
query: context2.params.query,
operationName: context2.params.operationName
});
return ({ result }) => done(result);
}
return void 0;
},
onParse(onParsePayload) {
const shouldTracePrase = typeof options.spans?.graphqlParse === "function" ? options.spans.graphqlParse(onParsePayload) : options.spans?.graphqlParse ?? true;
const { context: context2 } = onParsePayload;
const otelContext = requestContextMapping.get(context2.request);
if (shouldTracePrase && otelContext) {
const { done } = createGraphQLParseSpan({
otelContext,
tracer,
query: context2.params.query,
operationName: context2.params.operationName
});
return ({ result }) => done(result);
}
return void 0;
},
onExecute(onExecuteArgs) {
const shouldTraceExecute = typeof options.spans?.graphqlExecute === "function" ? options.spans.graphqlExecute(onExecuteArgs) : options.spans?.graphqlExecute ?? true;
const { args } = onExecuteArgs;
const otelContext = requestContextMapping.get(args.contextValue.request);
if (shouldTraceExecute && otelContext) {
const { done } = createGraphQLExecuteSpan({
args,
otelContext,
tracer
});
return {
onExecuteDone: ({ result }) => {
if (!isAsyncIterable(result)) {
done(result);
}
}
};
}
return void 0;
},
onSubgraphExecute(onSubgraphPayload) {
const shouldTraceSubgraphExecute = typeof options.spans?.subgraphExecute === "function" ? options.spans.subgraphExecute(onSubgraphPayload) : options.spans?.subgraphExecute ?? true;
const otelContext = onSubgraphPayload.executionRequest.context?.request ? requestContextMapping.get(
onSubgraphPayload.executionRequest.context.request
) : void 0;
if (shouldTraceSubgraphExecute && otelContext) {
const { subgraphName, executionRequest } = onSubgraphPayload;
const { done } = createSubgraphExecuteFetchSpan({
otelContext,
tracer,
executionRequest,
subgraphName
});
return done;
}
return void 0;
},
onFetch(onFetchPayload) {
const shouldTraceFetch = typeof options.spans?.upstreamFetch === "function" ? options.spans.upstreamFetch(onFetchPayload) : options.spans?.upstreamFetch ?? true;
const {
context: context2,
options: fetchOptions,
url,
setOptions,
executionRequest
} = onFetchPayload;
const otelContext = requestContextMapping.get(context2.request);
if (shouldTraceFetch && otelContext) {
if (propagateContext) {
const reqHeaders = getHeadersObj(fetchOptions.headers || {});
propagation.inject(otelContext, reqHeaders);
setOptions({
...fetchOptions,
headers: reqHeaders
});
}
const { done } = createUpstreamHttpFetchSpan({
otelContext,
tracer,
url,
fetchOptions,
executionRequest
});
return (fetchDonePayload) => done(fetchDonePayload.response);
}
return void 0;
},
onResponse({ request, response }) {
const otelContext = requestContextMapping.get(request);
if (!otelContext) {
return;
}
const rootSpan = trace.getSpan(otelContext);
if (rootSpan) {
completeHttpSpan(rootSpan, response);
}
requestContextMapping.delete(request);
},
async onDispose() {
if (spanProcessors) {
await Promise.all(
spanProcessors.map((processor) => processor.forceFlush())
);
}
await provider?.forceFlush?.();
if (spanProcessors) {
spanProcessors.forEach((processor) => processor.shutdown());
}
await provider?.shutdown?.();
diag.disable();
trace.disable();
context.disable();
propagation.disable();
}
};
}
export { createAzureMonitorExporter, createOtlpGrpcExporter, createOtlpHttpExporter, createStdoutExporter, createZipkinExporter, useOpenTelemetry };