Skip to content
31 changes: 30 additions & 1 deletion e2e/opentelemetry/gateway.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { defineConfig, GatewayPlugin } from '@graphql-hive/gateway';
import { trace } from '@graphql-hive/gateway/opentelemetry/api';
import { openTelemetrySetup } from '@graphql-hive/gateway/opentelemetry/setup';
import {
HiveTracingSpanProcessor,

Check failure on line 4 in e2e/opentelemetry/gateway.config.ts

View workflow job for this annotation

GitHub Actions / Types

'HiveTracingSpanProcessor' is declared but its value is never read.
openTelemetrySetup,
} from '@graphql-hive/gateway/opentelemetry/setup';
import type { MeshFetchRequestInit } from '@graphql-mesh/types';
import {
getNodeAutoInstrumentations,
Expand Down Expand Up @@ -29,6 +32,7 @@
};
};

//*
if (process.env['DISABLE_OPENTELEMETRY_SETUP'] !== '1') {
const { OTLPTraceExporter } =
process.env['OTLP_EXPORTER_TYPE'] === 'http'
Expand Down Expand Up @@ -74,6 +78,31 @@
});
}
}
/*/

const resource = resources.resourceFromAttributes({
'custom.resource': 'custom value',
});
const { OTLPTraceExporter } =
process.env['OTLP_EXPORTER_TYPE'] === 'http'
? await import(`@opentelemetry/exporter-trace-otlp-http`)
: await import(`@opentelemetry/exporter-trace-otlp-grpc`);

const exporter = new OTLPTraceExporter({
url: process.env['OTLP_EXPORTER_URL'],
});
openTelemetrySetup({
contextManager: new AsyncLocalStorageContextManager(),
resource,
traces: {
processors: [
new HiveTracingSpanProcessor({
processor: new tracing.SimpleSpanProcessor(exporter),
}),
],
},
});
//*/

export const gatewayConfig = defineConfig({
openTelemetry: {
Expand Down
113 changes: 113 additions & 0 deletions e2e/opentelemetry/opentelemetry.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,119 @@ describe('OpenTelemetry', () => {
});
});

it.only('should report telemetry metrics correctly to jaeger', async () => {
const serviceName = crypto.randomUUID();
const { execute } = await gateway({
supergraph,
env: {
OTLP_EXPORTER_TYPE,
OTLP_EXPORTER_URL: urls[OTLP_EXPORTER_TYPE],
OTEL_SERVICE_NAME: serviceName,
OTEL_SERVICE_VERSION: '1.0.0',
},
});

/*
await expect(execute({ query: exampleSetup.query })).resolves.toEqual(
exampleSetup.result,
);
/*/
await expect(
execute({ query: 'query test { unknown }' }),
).resolves.toEqual({
errors: [
{
message: 'Cannot query field "unknown" on type "Query".',
extensions: {
code: 'GRAPHQL_VALIDATION_FAILED',
},
locations: [
{
column: 14,
line: 1,
},
],
},
],
});
//*/
await expectJaegerTraces(serviceName, (traces) => {
const relevantTraces = traces.data.filter((trace) =>
trace.spans.some((span) =>
span.operationName.startsWith('graphql.operation'),
),
);
expect(relevantTraces.length).toBe(1);
const relevantTrace = relevantTraces[0];
expect(relevantTrace).toBeDefined();

// const resource = relevantTrace!.processes['p1'];
// expect(resource).toBeDefined();

// const tags = resource!.tags.map(({ key, value }) => ({ key, value }));
// // const tagKeys = resource!.tags.map(({ key }) => key);
// expect(resource!.serviceName).toBe(serviceName);
// [
// ['custom.resource', 'custom value'],
// ['otel.library.name', 'gateway'],
// ].forEach(([key, value]) => {
// return expect(tags).toContainEqual({ key, value });
// });

// if (
// gatewayRunner === 'node' ||
// gatewayRunner === 'docker' ||
// gatewayRunner === 'bin'
// ) {
// const expectedTags = [
// 'process.owner',
// 'host.arch',
// 'os.type',
// 'service.instance.id',
// ];
// if (gatewayRunner.includes('docker')) {
// expectedTags.push('container.id');
// }
// expectedTags.forEach((key) => {
// return expect(tags).toContainEqual(
// expect.objectContaining({ key }),
// );
// });
// }

// const spanTree = buildSpanTree(relevantTrace!.spans, 'POST /graphql');
// expect(spanTree).toBeDefined();

// expect(spanTree!.children).toHaveLength(1);

const operationSpan = buildSpanTree(
relevantTrace!.spans,
'graphql.operation',
)!;
const expectedOperationChildren = [
'graphql.parse',
'graphql.validate',
];
// expect(operationSpan!.children).toHaveLength(
// expectedOperationChildren.length,
// );
for (const operationName of expectedOperationChildren) {
expect(operationSpan?.children).toContainEqual(
expect.objectContaining({
span: expect.objectContaining({ operationName }),
}),
);
}

console.log(operationSpan.span.tags);
expect(
operationSpan.span.tags.find(
({ key }) => key === 'graphql.operation.name',
),
).toMatchObject({ value: 'TestQuery' });
});
});

it('should report telemetry metrics correctly to jaeger using cli options', async () => {
const serviceName = crypto.randomUUID();
const { execute } = await gateway({
Expand Down
39 changes: 23 additions & 16 deletions packages/plugins/opentelemetry/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { getHeadersObj } from '@graphql-mesh/utils';
import { ExecutionRequest, fakePromise } from '@graphql-tools/utils';
import { unfakePromise } from '@whatwg-node/promise-helpers';
import { DocumentNode } from 'graphql';

Check failure on line 11 in packages/plugins/opentelemetry/src/plugin.ts

View workflow job for this annotation

GitHub Actions / Types

'DocumentNode' is declared but its value is never read.
import {
context,
hive,
Expand Down Expand Up @@ -40,7 +41,7 @@
recordCacheError,
recordCacheEvent,
registerException,
setExecutionAttributesOnOperationSpan,
setDocumentAttributesOnOperationSpan,
setExecutionResultAttributes,
setGraphQLExecutionAttributes,
setGraphQLExecutionResultAttributes,
Expand Down Expand Up @@ -483,12 +484,7 @@

const { forOperation } = state;
forOperation.otel!.push(
createGraphQLValidateSpan({
ctx: getContext(state),
tracer,
query: gqlCtx.params.query?.trim(),
operationName: gqlCtx.params.operationName,
}),
createGraphQLValidateSpan({ ctx: getContext(state), tracer }),
);

if (useContextManager) {
Expand Down Expand Up @@ -800,10 +796,17 @@
query: gqlCtx.params.query?.trim(),
result,
});
if (!(result instanceof Error)) {
setDocumentAttributesOnOperationSpan({
ctx: state.forOperation.otel!.root,
document: result,
operationName: gqlCtx.params.operationName,
});
}
};
},

onValidate({ state, context: gqlCtx }) {
onValidate({ state, context: gqlCtx, params }) {
if (
!isParentEnabled(state) ||
!shouldTrace(traces.spans?.graphqlValidate, { context: gqlCtx })
Expand All @@ -812,7 +815,12 @@
}

return ({ result }) => {
setGraphQLValidateAttributes({ ctx: getContext(state), result });
setGraphQLValidateAttributes({
ctx: getContext(state),
result,
document: params.documentAST,
operationName: gqlCtx.params.operationName,
});
};
},

Expand All @@ -821,18 +829,17 @@
return;
}

setExecutionAttributesOnOperationSpan({
ctx: state.forOperation.otel!.root,
args,
hashOperationFn: options.hashOperation,
});

if (state.forOperation.skipExecuteSpan) {
return;
}

const ctx = getContext(state);
setGraphQLExecutionAttributes({ ctx, args });
setGraphQLExecutionAttributes({
ctx,
operationCtx: state.forOperation.otel!.root,
args,
hashOperationFn: options.hashOperation,
});

state.forOperation.subgraphNames = [];

Expand Down
Loading
Loading