UNPKG

@apollo/gateway

Version:
268 lines (237 loc) 9.26 kB
import type { Attributes, Exception, Span } from '@opentelemetry/api'; import opentelemetry from '@opentelemetry/api'; import type { GatewayGraphQLRequestContext } from '@apollo/server-gateway-interface'; import { cpuCountSync } from 'node-cpu-count'; import * as os from 'node:os'; import { OperationContext } from '../operationContext'; import {ATTR_OS_TYPE, ATTR_HOST_ARCH, OS_TYPE_VALUE_LINUX, ATTR_OS_NAME, METRIC_SYSTEM_MEMORY_LIMIT, METRIC_SYSTEM_CPU_LOGICAL_COUNT, METRIC_SYSTEM_CPU_FREQUENCY, ATTR_SERVICE_NAME, ATTR_SERVICE_INSTANCE_ID, METRIC_PROCESS_UPTIME, ATTR_CLOUD_PROVIDER, ATTR_CLOUD_PLATFORM} from '@opentelemetry/semantic-conventions/incubating'; import { MeterProvider, PeriodicExportingMetricReader, PushMetricExporter, } from '@opentelemetry/sdk-metrics'; import { alibabaCloudEcsDetector } from '@opentelemetry/resource-detector-alibaba-cloud'; import { awsBeanstalkDetector, awsEc2Detector, awsEcsDetector, awsEksDetector, awsLambdaDetector, } from '@opentelemetry/resource-detector-aws'; import { gcpDetector } from '@opentelemetry/resource-detector-gcp'; import { azureAppServiceDetector, azureFunctionsDetector, azureVmDetector, } from '@opentelemetry/resource-detector-azure'; import { detectResourcesSync, hostDetectorSync, osDetectorSync, Resource } from '@opentelemetry/resources'; import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'; import { randomUUID } from 'node:crypto'; export type OpenTelemetryConfig = { /** * Whether to include the `graphql.document` attribute in the `gateway.request` OpenTelemetry spans. * When set to `true`, the attribute will contain the entire GraphQL document for the current request. * * Defaults to `false`, meaning that the GraphQL document will not be added as a span attribute. */ includeDocument?: boolean; /** * Whether to record the GraphQL and internal errors that take place while processing a request as * exception events in the OpenTelemetry spans in which they occur. * * When a number is given as a value, it represents the maximum number of exceptions that will be * reported in each OpenTelemetry span. * * Regardless of the value of this setting, the span status code will be set to `ERROR` when a GraphQL * or internal error occurs. * * Defaults to `false`, meaning that no exceptions will be reported in any spans. */ recordExceptions?: boolean | number; }; export enum OpenTelemetrySpanNames { REQUEST = 'gateway.request', PLAN = 'gateway.plan', FETCH = 'gateway.fetch', POST_PROCESSING = 'gateway.postprocessing', EXECUTE = 'gateway.execute', VALIDATE = 'gateway.validate', } /* When adding any more, please refer to: https://opentelemetry.io/docs/specs/otel/common/attribute-naming/ https://opentelemetry.io/docs/specs/otel/trace/semantic_conventions/instrumentation/graphql/ */ export enum OpenTelemetryAttributeNames { GRAPHQL_DOCUMENT = 'graphql.document', GRAPHQL_OPERATION_NAME = 'graphql.operation.name', GRAPHQL_OPERATION_NAME_DEPRECATED = 'operationName', // deprecated in favor of GRAPHQL_OPERATION_NAME GRAPHQL_OPERATION_TYPE = 'graphql.operation.type', } const APOLLO_OTEL_ENDPOINT = "https://usage-reporting.api.apollographql.com"; const EXPORT_INTERVAL_SECONDS = 60 * 60; // one hour const EXPORT_INTERVAL_MILLIS = 1000 * EXPORT_INTERVAL_SECONDS; const NAME = "apollo/gateway-js"; const SI_MEGA = 1000000; const { version } = require('../../package.json'); export const tracer = opentelemetry.trace.getTracer(NAME, version); export interface SpanAttributes extends Attributes { /** * @deprecated in favor of `graphql.operation.name` */ [OpenTelemetryAttributeNames.GRAPHQL_OPERATION_NAME_DEPRECATED]?: string; [OpenTelemetryAttributeNames.GRAPHQL_OPERATION_NAME]?: string; [OpenTelemetryAttributeNames.GRAPHQL_OPERATION_TYPE]?: string; [OpenTelemetryAttributeNames.GRAPHQL_DOCUMENT]?: string; } export function requestContextSpanAttributes( requestContext: GatewayGraphQLRequestContext, config: OpenTelemetryConfig | undefined ): SpanAttributes { const spanAttributes: SpanAttributes = {}; if (requestContext.operationName) { spanAttributes[ OpenTelemetryAttributeNames.GRAPHQL_OPERATION_NAME_DEPRECATED ] = requestContext.operationName; spanAttributes[OpenTelemetryAttributeNames.GRAPHQL_OPERATION_NAME] = requestContext.operationName; } if (config?.includeDocument && requestContext.source) { spanAttributes[OpenTelemetryAttributeNames.GRAPHQL_DOCUMENT] = requestContext.source; } return spanAttributes; } export function operationContextSpanAttributes( operationContext: OperationContext ): SpanAttributes { const spanAttributes: SpanAttributes = {}; if (operationContext.operation.operation) { spanAttributes[OpenTelemetryAttributeNames.GRAPHQL_OPERATION_TYPE] = operationContext.operation.operation; } return spanAttributes; } export function recordExceptions( span: Span, exceptions: readonly Exception[], config: OpenTelemetryConfig | undefined ) { const recordExceptions = config?.recordExceptions; if (recordExceptions === undefined || recordExceptions === false) { return; } let exceptionsToRecord; if (recordExceptions === true) { exceptionsToRecord = exceptions; } else if (recordExceptions <= 0) { return; } else { exceptionsToRecord = exceptions.slice(0, recordExceptions) } for (const exception of exceptionsToRecord) { span.recordException(exception); } } // Non-identifying cloud resource attributes that we process. const ALLOWED_CLOUD_RESOURCE_ATTRIBUTES: Set<string> = new Set([ ATTR_CLOUD_PROVIDER, ATTR_CLOUD_PLATFORM, ]); function filterToAllowedCloudAttributes(resource: Resource): Resource { return new Resource( {}, Promise.resolve(resource.waitForAsyncAttributes?.()).then(() => { const filtered: Attributes = {}; for (const [key, value] of Object.entries(resource.attributes)) { if (ALLOWED_CLOUD_RESOURCE_ATTRIBUTES.has(key)) { filtered[key] = value; } } return filtered; }), ); } // Exposed for unit testing with a testable exporter export function createDataCollectionMeterProvider(metricExporter: PushMetricExporter): MeterProvider { const detectedResource = detectResourcesSync({ detectors: [ alibabaCloudEcsDetector, awsEc2Detector, awsBeanstalkDetector, awsEcsDetector, awsEksDetector, awsLambdaDetector, gcpDetector, azureVmDetector, azureFunctionsDetector, azureAppServiceDetector ], }) const resource = filterToAllowedCloudAttributes(detectedResource); const meterProvider = new MeterProvider({ resource: resource.merge( new Resource ({ [ATTR_SERVICE_NAME]: 'gateway-js', [ATTR_SERVICE_INSTANCE_ID]: randomUUID() }), ), readers: [ new PeriodicExportingMetricReader({ exporter: metricExporter, exportIntervalMillis: EXPORT_INTERVAL_MILLIS, }), ], }) // grab the host and OS attributes explicitly so we can create `linux.distribution` const hostAttrs = hostDetectorSync.detect().attributes const osAttrs = osDetectorSync.detect().attributes const osType = osAttrs[ATTR_OS_TYPE] const hostArch = hostAttrs[ATTR_HOST_ARCH] const meter = meterProvider.getMeter(NAME, version) const uptimeGauge = meter.createObservableGauge(METRIC_PROCESS_UPTIME, { "description": "The uptime of the JS federation gateway running" }) const instanceAttrs: Attributes = { [ATTR_OS_TYPE]: osType, [ATTR_HOST_ARCH]: hostArch } if (osType === OS_TYPE_VALUE_LINUX) { instanceAttrs["linux.distribution"] = osAttrs[ATTR_OS_NAME]; } let uptime = 0; uptimeGauge.addCallback((result) => { result.observe(uptime, instanceAttrs) uptime = uptime + EXPORT_INTERVAL_SECONDS; }) const cpuFreqGauge = meter.createObservableGauge(METRIC_SYSTEM_CPU_FREQUENCY, { "description": "The CPU frequency of the underlying instance the JS federation gateway is deployed to", "unit": "Hz" }) cpuFreqGauge.addCallback((result) => { const cpus = os.cpus(); const average_frequency = os.cpus().map((a) => a.speed * SI_MEGA).reduce((partialSum, a) => partialSum + a, 0) / cpus.length result.observe(average_frequency) }) const cpuCountGauge = meter.createObservableGauge(METRIC_SYSTEM_CPU_LOGICAL_COUNT, { "description": "The number of CPUs reported by the instance the JS federation gateway is running on" }) cpuCountGauge.addCallback((result) => { result.observe(cpuCountSync(), { [ATTR_HOST_ARCH]: hostArch, }) }) const totalMemoryGauge = meter.createObservableGauge(METRIC_SYSTEM_MEMORY_LIMIT, { "description": "The amount of memory reported by the instance the JS federation gateway is running on", "unit": "bytes" }) totalMemoryGauge.addCallback((result) => { result.observe(os.totalmem(), { [ATTR_HOST_ARCH]: hostArch, }) }) return meterProvider } export function createDataCollectionExporter(): MeterProvider { return createDataCollectionMeterProvider(new OTLPMetricExporter({ url: APOLLO_OTEL_ENDPOINT })); }