Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 99 additions & 19 deletions deployment/config/public-graphql-api-gateway/gateway.config.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
const promises: Promise<void>[] = [];

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<void> {
const promises: Promise<void>[] = [];

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([
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this impact performance at all, such that we shouldn't add it if only one of the exporters is used?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably minimal, but yes it always adds a minimal impact. When nothing is configured, OTEL uses a no-op TracerProvider which just does nothing at all.

...(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.
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions deployment/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ const publicGraphQLAPIGateway = deployPublicGraphQLAPIGateway({
graphql,
docker,
observability,
otelCollector,
});

const proxy = deployProxy({
Expand Down
17 changes: 16 additions & 1 deletion deployment/services/public-graphql-api-gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -32,6 +33,7 @@ export function deployPublicGraphQLAPIGateway(args: {
graphql: GraphQL;
docker: Docker;
observability: Observability;
otelCollector: OTELCollector;
}) {
const apiConfig = new pulumi.Config('api');

Expand All @@ -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
Expand All @@ -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(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why double: HIVE_HIVE_ ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because HIVE_TRACE_ENDPOINT has side effects, hive-gateway will auto-setup tracing in that case, which leads to setting up insturmentation twice, which then prints error to the console.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's actually only on HIVE_TRACE_ACCESS_TOKEN, so HIVE_TRACE_ENDPOINT is fine. But for coherence, probably better to have the same prefix everywhere :-P

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'],
Expand Down Expand Up @@ -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();
}

Expand Down
Loading