diff --git a/deployment/config/public-graphql-api-gateway/gateway.config.ts b/deployment/config/public-graphql-api-gateway/gateway.config.ts index e142567e76..186f451c49 100644 --- a/deployment/config/public-graphql-api-gateway/gateway.config.ts +++ b/deployment/config/public-graphql-api-gateway/gateway.config.ts @@ -1,24 +1,102 @@ // @ts-expect-error not a dependency import { defineConfig } from '@graphql-hive/gateway'; // @ts-expect-error not a dependency -import { openTelemetrySetup } from '@graphql-hive/gateway/opentelemetry/setup'; +import { hiveTracingSetup } from '@graphql-hive/plugin-opentelemetry/setup'; +import type { Context } from '@opentelemetry/api'; import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; +import { globalErrorHandler } from '@opentelemetry/core'; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; +import { + BatchSpanProcessor, + SpanProcessor, + type ReadableSpan, + type Span, +} from '@opentelemetry/sdk-trace-base'; -openTelemetrySetup({ - // Mandatory: It depends on the available API in your runtime. - // We recommend AsyncLocalStorage based manager when possible. - // `@opentelemetry/context-zone` is also available for other runtimes. - // Pass `false` to disable context manager usage. - contextManager: new AsyncLocalStorageContextManager(), +/** Note: this is inlined for now... */ +class MultiSpanProcessor implements SpanProcessor { + constructor(private readonly _spanProcessors: SpanProcessor[]) {} - traces: { - // Define your exporter, most of the time the OTLP HTTP one. Traces are batched by default. - exporter: new OTLPTraceExporter({ url: process.env['OPENTELEMETRY_COLLECTOR_ENDPOINT']! }), - // You can easily enable a console exporter for quick debug - console: process.env['DEBUG_TRACES'] === '1', - }, -}); + forceFlush(): Promise { + const promises: Promise[] = []; + + for (const spanProcessor of this._spanProcessors) { + promises.push(spanProcessor.forceFlush()); + } + return new Promise(resolve => { + Promise.all(promises) + .then(() => { + resolve(); + }) + .catch(error => { + globalErrorHandler(error || new Error('MultiSpanProcessor: forceFlush failed')); + resolve(); + }); + }); + } + + onStart(span: Span, context: Context): void { + for (const spanProcessor of this._spanProcessors) { + spanProcessor.onStart(span, context); + } + } + + onEnd(span: ReadableSpan): void { + for (const spanProcessor of this._spanProcessors) { + spanProcessor.onEnd(span); + } + } + + shutdown(): Promise { + const promises: Promise[] = []; + + for (const spanProcessor of this._spanProcessors) { + promises.push(spanProcessor.shutdown()); + } + return new Promise((resolve, reject) => { + Promise.all(promises).then(() => { + resolve(); + }, reject); + }); + } +} + +if ( + process.env['OPENTELEMETRY_COLLECTOR_ENDPOINT'] || + process.env['HIVE_HIVE_TRACE_ACCESS_TOKEN'] +) { + hiveTracingSetup({ + // Noop is only there to not raise an exception in case we do not hive console tracing. + target: process.env['HIVE_HIVE_TARGET'] ?? 'noop', + contextManager: new AsyncLocalStorageContextManager(), + processor: new MultiSpanProcessor([ + ...(process.env['HIVE_HIVE_TRACE_ACCESS_TOKEN'] && + process.env['HIVE_HIVE_TRACE_ENDPOINT'] && + process.env['HIVE_HIVE_TARGET'] + ? [ + new BatchSpanProcessor( + new OTLPTraceExporter({ + url: process.env['HIVE_HIVE_TRACE_ENDPOINT'], + headers: { + Authorization: `Bearer ${process.env['HIVE_HIVE_TRACE_ACCESS_TOKEN']}`, + 'X-Hive-Target-Ref': process.env['HIVE_HIVE_TARGET'], + }, + }), + ), + ] + : []), + ...(process.env['OPENTELEMETRY_COLLECTOR_ENDPOINT'] + ? [ + new BatchSpanProcessor( + new OTLPTraceExporter({ + url: process.env['OPENTELEMETRY_COLLECTOR_ENDPOINT']!, + }), + ), + ] + : []), + ]), + }); +} const defaultQuery = `# # Welcome to the Hive Console GraphQL API. @@ -50,11 +128,13 @@ export const gatewayConfig = defineConfig({ }, disableWebsockets: true, prometheus: true, - openTelemetry: process.env['OPENTELEMETRY_COLLECTOR_ENDPOINT'] - ? { - serviceName: 'public-graphql-api-gateway', - } - : false, + openTelemetry: + process.env['OPENTELEMETRY_COLLECTOR_ENDPOINT'] || process.env['HIVE_HIVE_TRACE_ACCESS_TOKEN'] + ? { + traces: true, + serviceName: 'public-graphql-api-gateway', + } + : undefined, demandControl: { maxCost: 1000, includeExtensionMetadata: true, diff --git a/deployment/index.ts b/deployment/index.ts index d487752fb6..159e2cce5d 100644 --- a/deployment/index.ts +++ b/deployment/index.ts @@ -307,6 +307,7 @@ const publicGraphQLAPIGateway = deployPublicGraphQLAPIGateway({ graphql, docker, observability, + otelCollector, }); const proxy = deployProxy({ diff --git a/deployment/services/public-graphql-api-gateway.ts b/deployment/services/public-graphql-api-gateway.ts index 8fbf76bcfe..867fe650da 100644 --- a/deployment/services/public-graphql-api-gateway.ts +++ b/deployment/services/public-graphql-api-gateway.ts @@ -9,12 +9,13 @@ import { type Docker } from './docker'; import { type Environment } from './environment'; import { type GraphQL } from './graphql'; import { type Observability } from './observability'; +import { type OTELCollector } from './otel-collector'; /** * Hive Gateway Docker Image Version * Bump this to update the used gateway version. */ -const dockerImage = 'ghcr.io/graphql-hive/gateway:2.1.8'; +const dockerImage = 'ghcr.io/graphql-hive/gateway:2.1.10'; const gatewayConfigDirectory = path.resolve( __dirname, @@ -32,6 +33,7 @@ export function deployPublicGraphQLAPIGateway(args: { graphql: GraphQL; docker: Docker; observability: Observability; + otelCollector: OTELCollector; }) { const apiConfig = new pulumi.Config('api'); @@ -43,6 +45,11 @@ export function deployPublicGraphQLAPIGateway(args: { throw new Error("Missing cdn endpoint variable 'HIVE_PERSISTED_DOCUMENTS_CDN_ENDPOINT'."); } + const hiveConfig = new pulumi.Config('hive'); + const hiveConfigSecrets = new ServiceSecret('hive-secret', { + otelTraceAccessToken: hiveConfig.requireSecret('otelTraceAccessToken'), + }); + const supergraphEndpoint = cdnEndpoint + '/contracts/public'; // Note: The persisted documents access key is also valid for reading the supergraph @@ -69,6 +76,13 @@ export function deployPublicGraphQLAPIGateway(args: { ), SUPERGRAPH_ENDPOINT: supergraphEndpoint, OPENTELEMETRY_COLLECTOR_ENDPOINT: args.observability.tracingEndpoint ?? '', + + // Hive Console OTEL Tracing configuration + HIVE_HIVE_TRACE_ENDPOINT: serviceLocalEndpoint(args.otelCollector.service).apply( + value => `${value}/v1/traces`, + ), + HIVE_HIVE_TARGET: hiveConfig.require('target'), + // HIVE_TRACE_ACCESS_TOKEN is a secret }, port: 4000, args: ['-c', '/config/gateway.config.ts', 'supergraph'], @@ -100,6 +114,7 @@ export function deployPublicGraphQLAPIGateway(args: { [args.graphql.deployment, args.graphql.service], ) .withSecret('HIVE_CDN_ACCESS_TOKEN', publicGraphQLAPISecret, 'cdnAccessKeyId') + .withSecret('HIVE_HIVE_TRACE_ACCESS_TOKEN', hiveConfigSecrets, 'otelTraceAccessToken') .deploy(); }