@apollo/gateway
Version:
268 lines (237 loc) • 9.26 kB
text/typescript
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 }));
}