diff --git a/examples/$/helpers.ts b/examples/$/helpers.ts index 4e57ee041..6188a844e 100644 --- a/examples/$/helpers.ts +++ b/examples/$/helpers.ts @@ -2,7 +2,8 @@ import getPort from 'get-port' import type { GraphQLSchema } from 'graphql' import { createYoga } from 'graphql-yoga' import { createServer } from 'node:http' -import { inspect } from 'node:util' + +export * from './show.js' export const documentQueryContinents = { document: `query { continents { name } }`, @@ -12,18 +13,6 @@ export const publicGraphQLSchemaEndpoints = { Atlas: `https://countries.trevorblades.com/graphql`, } -export const showPartition = `---------------------------------------- SHOW ----------------------------------------` - -export const show = (value: unknown) => { - console.log(showPartition) - console.log(inspect(value, { depth: null, colors: true })) -} - -export const showJson = (value: unknown) => { - console.log(showPartition) - console.log(JSON.stringify(value, null, 2)) -} - export const serveSchema = async (input: { schema: GraphQLSchema }) => { const { schema } = input const yoga = createYoga({ schema, logging: false, maskedErrors: false }) diff --git a/examples/$/show.ts b/examples/$/show.ts new file mode 100644 index 000000000..1fc6bb16a --- /dev/null +++ b/examples/$/show.ts @@ -0,0 +1,39 @@ +import { inspect } from 'node:util' + +const originalWrite = globalThis.process.stdout.write.bind(globalThis.process.stdout) + +type Logger = typeof console.log | typeof globalThis.process.stdout.write + +export const show = <$Logger extends Logger = typeof console.log>( + value: unknown, + logger?: $Logger, +): ReturnType<$Logger> => { + const write = logger ?? console.log + const inspected = inspect(value, { depth: null, colors: true }) + const message = renderShow(inspected) + return write(message) as ReturnType<$Logger> +} + +export const showJson = <$Logger extends Logger = typeof console.log>( + value: unknown, + logger?: $Logger, +): ReturnType<$Logger> => { + const write = logger ?? console.log + const encoded = JSON.stringify(value, null, 2) + const message = renderShow(encoded) + return write(message) as ReturnType<$Logger> +} + +export const showPartition = `---------------------------------------- SHOW ----------------------------------------` + +const renderShow = (value: string) => { + return showPartition + '\n' + value +} + +export const interceptAndShowOutput = (): void => { + globalThis.process.stdout.write = (value) => { + if (typeof value !== `string`) return originalWrite(value) + if (value.includes(showPartition)) return originalWrite(value) + return originalWrite(renderShow(value)) + } +} diff --git a/examples/__outputs__/extension|extension_opentelemetry.output.encoder.ts b/examples/__outputs__/extension|extension_opentelemetry.output.encoder.ts new file mode 100644 index 000000000..e99966675 --- /dev/null +++ b/examples/__outputs__/extension|extension_opentelemetry.output.encoder.ts @@ -0,0 +1,10 @@ +export const encode = (value: string) => { + return value + .replaceAll(/(id: )'.+'/g, `$1'...'`) + .replaceAll(/(traceId: )'.+'/g, `$1'...'`) + .replaceAll(/(parentId: )'.+'/g, `$1'...'`) + .replaceAll(/(timestamp: ).+,/g, `$10,`) + .replaceAll(/(duration: ).+,/g, `$10.0,`) + .replaceAll(/(service\.name': )'.+'/g, `$1'...'`) + .replaceAll(/('telemetry\.sdk\.version': )'.+'/g, `$1'...'`) +} diff --git a/examples/__outputs__/extension|extension_opentelemetry.output.test.txt b/examples/__outputs__/extension|extension_opentelemetry.output.test.txt new file mode 100644 index 000000000..5bfe1dd0c --- /dev/null +++ b/examples/__outputs__/extension|extension_opentelemetry.output.test.txt @@ -0,0 +1,156 @@ +---------------------------------------- SHOW ---------------------------------------- +{ + resource: { + attributes: { + 'service.name': '...', + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': '...' + } + }, + instrumentationScope: { name: 'graffle', version: undefined, schemaUrl: undefined }, + traceId: '...', + parentId: '...', + traceState: undefined, + name: 'encode', + id: '...', + kind: 0, + timestamp: 0, + duration: 0.0, + attributes: {}, + status: { code: 0 }, + events: [], + links: [] +} +---------------------------------------- SHOW ---------------------------------------- +{ + resource: { + attributes: { + 'service.name': '...', + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': '...' + } + }, + instrumentationScope: { name: 'graffle', version: undefined, schemaUrl: undefined }, + traceId: '...', + parentId: '...', + traceState: undefined, + name: 'pack', + id: '...', + kind: 0, + timestamp: 0, + duration: 0.0, + attributes: {}, + status: { code: 0 }, + events: [], + links: [] +} +---------------------------------------- SHOW ---------------------------------------- +{ + resource: { + attributes: { + 'service.name': '...', + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': '...' + } + }, + instrumentationScope: { name: 'graffle', version: undefined, schemaUrl: undefined }, + traceId: '...', + parentId: '...', + traceState: undefined, + name: 'exchange', + id: '...', + kind: 0, + timestamp: 0, + duration: 0.0, + attributes: {}, + status: { code: 0 }, + events: [], + links: [] +} +---------------------------------------- SHOW ---------------------------------------- +{ + resource: { + attributes: { + 'service.name': '...', + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': '...' + } + }, + instrumentationScope: { name: 'graffle', version: undefined, schemaUrl: undefined }, + traceId: '...', + parentId: '...', + traceState: undefined, + name: 'unpack', + id: '...', + kind: 0, + timestamp: 0, + duration: 0.0, + attributes: {}, + status: { code: 0 }, + events: [], + links: [] +} +---------------------------------------- SHOW ---------------------------------------- +{ + resource: { + attributes: { + 'service.name': '...', + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': '...' + } + }, + instrumentationScope: { name: 'graffle', version: undefined, schemaUrl: undefined }, + traceId: '...', + parentId: '...', + traceState: undefined, + name: 'decode', + id: '...', + kind: 0, + timestamp: 0, + duration: 0.0, + attributes: {}, + status: { code: 0 }, + events: [], + links: [] +} +---------------------------------------- SHOW ---------------------------------------- +{ + resource: { + attributes: { + 'service.name': '...', + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': '...' + } + }, + instrumentationScope: { name: 'graffle', version: undefined, schemaUrl: undefined }, + traceId: '...', + parentId: undefined, + traceState: undefined, + name: 'request', + id: '...', + kind: 0, + timestamp: 0, + duration: 0.0, + attributes: {}, + status: { code: 0 }, + events: [], + links: [] +} +---------------------------------------- SHOW ---------------------------------------- +{ + continents: [ + { name: 'Africa' }, + { name: 'Antarctica' }, + { name: 'Asia' }, + { name: 'Europe' }, + { name: 'North America' }, + { name: 'Oceania' }, + { name: 'South America' } + ] +} \ No newline at end of file diff --git a/examples/__outputs__/extension|extension_opentelemetry.output.txt b/examples/__outputs__/extension|extension_opentelemetry.output.txt new file mode 100644 index 000000000..7b2adc016 --- /dev/null +++ b/examples/__outputs__/extension|extension_opentelemetry.output.txt @@ -0,0 +1,156 @@ +---------------------------------------- SHOW ---------------------------------------- +{ + resource: { + attributes: { + 'service.name': 'unknown_service:/Users/jasonkuhrt/Library/pnpm/nodejs/22.7.0/bin/node', + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': '1.26.0' + } + }, + instrumentationScope: { name: 'graffle', version: undefined, schemaUrl: undefined }, + traceId: '93ece70aa164958fd1b3d3c6c89e9f70', + parentId: 'e3b3fd57f531674b', + traceState: undefined, + name: 'encode', + id: '0f8e4e2d2fa7a1d1', + kind: 0, + timestamp: 1726068743834000, + duration: 442.792, + attributes: {}, + status: { code: 0 }, + events: [], + links: [] +} +---------------------------------------- SHOW ---------------------------------------- +{ + resource: { + attributes: { + 'service.name': 'unknown_service:/Users/jasonkuhrt/Library/pnpm/nodejs/22.7.0/bin/node', + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': '1.26.0' + } + }, + instrumentationScope: { name: 'graffle', version: undefined, schemaUrl: undefined }, + traceId: '93ece70aa164958fd1b3d3c6c89e9f70', + parentId: 'e3b3fd57f531674b', + traceState: undefined, + name: 'pack', + id: '37766e2e0fa6ea2e', + kind: 0, + timestamp: 1726068743836000, + duration: 808.5, + attributes: {}, + status: { code: 0 }, + events: [], + links: [] +} +---------------------------------------- SHOW ---------------------------------------- +{ + resource: { + attributes: { + 'service.name': 'unknown_service:/Users/jasonkuhrt/Library/pnpm/nodejs/22.7.0/bin/node', + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': '1.26.0' + } + }, + instrumentationScope: { name: 'graffle', version: undefined, schemaUrl: undefined }, + traceId: '93ece70aa164958fd1b3d3c6c89e9f70', + parentId: 'e3b3fd57f531674b', + traceState: undefined, + name: 'exchange', + id: 'ed9ae7aad6fd1e69', + kind: 0, + timestamp: 1726068743837000, + duration: 329989.458, + attributes: {}, + status: { code: 0 }, + events: [], + links: [] +} +---------------------------------------- SHOW ---------------------------------------- +{ + resource: { + attributes: { + 'service.name': 'unknown_service:/Users/jasonkuhrt/Library/pnpm/nodejs/22.7.0/bin/node', + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': '1.26.0' + } + }, + instrumentationScope: { name: 'graffle', version: undefined, schemaUrl: undefined }, + traceId: '93ece70aa164958fd1b3d3c6c89e9f70', + parentId: 'e3b3fd57f531674b', + traceState: undefined, + name: 'unpack', + id: 'd0d9cbd74e358490', + kind: 0, + timestamp: 1726068744168000, + duration: 1907.291, + attributes: {}, + status: { code: 0 }, + events: [], + links: [] +} +---------------------------------------- SHOW ---------------------------------------- +{ + resource: { + attributes: { + 'service.name': 'unknown_service:/Users/jasonkuhrt/Library/pnpm/nodejs/22.7.0/bin/node', + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': '1.26.0' + } + }, + instrumentationScope: { name: 'graffle', version: undefined, schemaUrl: undefined }, + traceId: '93ece70aa164958fd1b3d3c6c89e9f70', + parentId: 'e3b3fd57f531674b', + traceState: undefined, + name: 'decode', + id: '51a5859eae82dd62', + kind: 0, + timestamp: 1726068744170000, + duration: 97.958, + attributes: {}, + status: { code: 0 }, + events: [], + links: [] +} +---------------------------------------- SHOW ---------------------------------------- +{ + resource: { + attributes: { + 'service.name': 'unknown_service:/Users/jasonkuhrt/Library/pnpm/nodejs/22.7.0/bin/node', + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': '1.26.0' + } + }, + instrumentationScope: { name: 'graffle', version: undefined, schemaUrl: undefined }, + traceId: '93ece70aa164958fd1b3d3c6c89e9f70', + parentId: undefined, + traceState: undefined, + name: 'request', + id: 'e3b3fd57f531674b', + kind: 0, + timestamp: 1726068743834000, + duration: 336135.125, + attributes: {}, + status: { code: 0 }, + events: [], + links: [] +} +---------------------------------------- SHOW ---------------------------------------- +{ + continents: [ + { name: 'Africa' }, + { name: 'Antarctica' }, + { name: 'Asia' }, + { name: 'Europe' }, + { name: 'North America' }, + { name: 'Oceania' }, + { name: 'South America' } + ] +} \ No newline at end of file diff --git a/examples/__outputs__/output|output_envelope.output.txt b/examples/__outputs__/output|output_envelope.output.txt index 3b1c6af3c..97bde0284 100644 --- a/examples/__outputs__/output|output_envelope.output.txt +++ b/examples/__outputs__/output|output_envelope.output.txt @@ -19,7 +19,7 @@ headers: Headers { connection: 'keep-alive', 'content-length': '119', - 'x-served-by': 'cache-yul1970023-YUL', + 'x-served-by': 'cache-yul1970045-YUL', 'accept-ranges': 'bytes', date: 'Sun, 08 Sep 2024 18:13:26 GMT', 'content-type': 'application/graphql-response+json; charset=utf-8', @@ -32,13 +32,13 @@ 'alt-svc': 'h3=":443"; ma=86400', 'access-control-allow-origin': '*', 'x-powered-by': 'Stellate', - age: '185021', + age: '249539', 'cache-control': 'public, s-maxage=2628000, stale-while-revalidate=2628000', 'x-cache': 'HIT', - 'x-cache-hits': '38', + 'x-cache-hits': '5', 'gcdn-cache': 'HIT', - 'stellate-rate-limit-budget-remaining': '36', - 'stellate-rate-limit-rules': '"IP limit";type="RequestCount";budget=50;limited=?0;remaining=36;refill=6', + 'stellate-rate-limit-budget-remaining': '41', + 'stellate-rate-limit-rules': '"IP limit";type="RequestCount";budget=50;limited=?0;remaining=41;refill=60', 'stellate-rate-limit-decision': 'pass', 'stellate-rate-limit-budget-required': '5', 'content-encoding': 'br' diff --git a/examples/__outputs__/transport-http|transport-http_extension_headers__dynamicHeaders.output.txt b/examples/__outputs__/transport-http|transport-http_extension_headers__dynamicHeaders.output.txt index e45dca79e..647e62759 100644 --- a/examples/__outputs__/transport-http|transport-http_extension_headers__dynamicHeaders.output.txt +++ b/examples/__outputs__/transport-http|transport-http_extension_headers__dynamicHeaders.output.txt @@ -4,7 +4,7 @@ headers: Headers { accept: 'application/graphql-response+json; charset=utf-8, application/json; charset=utf-8', 'content-type': 'application/json', - 'x-sent-at-time': '1726004226733' + 'x-sent-at-time': '1726068743905' }, signal: undefined, method: 'post', diff --git a/examples/extension|extension_opentelemetry.ts b/examples/extension|extension_opentelemetry.ts new file mode 100644 index 000000000..0329425e9 --- /dev/null +++ b/examples/extension|extension_opentelemetry.ts @@ -0,0 +1,20 @@ +import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base' +import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node' +import { Opentelemetry } from '../src/entrypoints/extensions.js' +import { Atlas } from './$/generated-clients/atlas/__.js' +import { interceptAndShowOutput, show } from './$/helpers.js' + +interceptAndShowOutput() + +// Setup Opentelemetry +// 1. Initialize the OpenTelemetry provider +// 2. Register the provider to make the OpenTelemetry API use it +const exporter = new ConsoleSpanExporter() +const processor = new SimpleSpanProcessor(exporter) +const provider = new NodeTracerProvider() +provider.addSpanProcessor(processor) +provider.register() + +const graffle = Atlas.create().use(Opentelemetry()) +const result = await graffle.rawString({ document: `query { continents { name } }` }) +show(result.data) diff --git a/package.json b/package.json index b6d46ebbf..0fd546ad9 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,11 @@ "default": "./build/entrypoints/generator.js" } }, + "./extensions": { + "import": { + "default": "./build/entrypoints/extensions.js" + } + }, "./client": { "import": { "default": "./build/entrypoints/client.js" @@ -91,6 +96,7 @@ "peerDependencies": { "@dprint/formatter": "^0.3.0 || ^0.4.0", "@dprint/typescript": "^0.91.1", + "@opentelemetry/api": "^1.9.0", "dprint": "^0.46.2 || ^0.47.0", "graphql": "14 - 16" }, @@ -98,6 +104,9 @@ "dprint": { "optional": true }, + "@opentelemetry/api": { + "optional": true + }, "@dprint/formatter": { "optional": true }, @@ -107,6 +116,9 @@ }, "devDependencies": { "@arethetypeswrong/cli": "^0.16.2", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/sdk-trace-base": "^1.26.0", + "@opentelemetry/sdk-trace-node": "^1.26.0", "@pothos/core": "^4.2.0", "@pothos/plugin-simple-objects": "^4.1.0", "@pothos/plugin-zod": "^4.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cebd0a909..8bfb3dfdd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,6 +30,15 @@ importers: '@arethetypeswrong/cli': specifier: ^0.16.2 version: 0.16.2 + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.0 + '@opentelemetry/sdk-trace-base': + specifier: ^1.26.0 + version: 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': + specifier: ^1.26.0 + version: 1.26.0(@opentelemetry/api@1.9.0) '@pothos/core': specifier: ^4.2.0 version: 4.2.0(graphql@16.9.0) @@ -853,6 +862,56 @@ packages: '@octokit/types@6.41.0': resolution: {integrity: sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==} + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/context-async-hooks@1.26.0': + resolution: {integrity: sha512-HedpXXYzzbaoutw6DFLWLDket2FwLkLpil4hGCZ1xYEIMTcivdfwEOISgdbLEWyG3HW52gTq2V9mOVJrONgiwg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@1.26.0': + resolution: {integrity: sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/propagator-b3@1.26.0': + resolution: {integrity: sha512-vvVkQLQ/lGGyEy9GT8uFnI047pajSOVnZI2poJqVGD3nJ+B9sFGdlHNnQKophE3lHfnIH0pw2ubrCTjZCgIj+Q==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/propagator-jaeger@1.26.0': + resolution: {integrity: sha512-DelFGkCdaxA1C/QA0Xilszfr0t4YbGd3DjxiCDPh34lfnFr+VkkrjV9S8ZTJvAzfdKERXhfOxIKBoGPJwoSz7Q==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/resources@1.26.0': + resolution: {integrity: sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@1.26.0': + resolution: {integrity: sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-trace-node@1.26.0': + resolution: {integrity: sha512-Fj5IVKrj0yeUwlewCRwzOVcr5avTuNnMHWf7GPc1t6WaT78J6CJyF3saZ/0RkZfdeNO8IcBl/bNcWMVZBMRW8Q==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.27.0': + resolution: {integrity: sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==} + engines: {node: '>=14'} + '@pothos/core@4.2.0': resolution: {integrity: sha512-dEoQCaA/pCPg3MhjGCwBF1QRbtOPysXm4nivQQsq6OrOvC7pU+KJNVdBSwpi1ZtTLtd4wg+PCC8Js9GrCyh9sw==} peerDependencies: @@ -4489,6 +4548,52 @@ snapshots: dependencies: '@octokit/openapi-types': 12.11.0 + '@opentelemetry/api@1.9.0': {} + + '@opentelemetry/context-async-hooks@1.26.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/core@1.26.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.27.0 + + '@opentelemetry/propagator-b3@1.26.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/propagator-jaeger@1.26.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/resources@1.26.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.27.0 + + '@opentelemetry/sdk-trace-base@1.26.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.27.0 + + '@opentelemetry/sdk-trace-node@1.26.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.26.0(@opentelemetry/api@1.9.0) + semver: 7.6.3 + + '@opentelemetry/semantic-conventions@1.27.0': {} + '@pothos/core@4.2.0(graphql@16.9.0)': dependencies: graphql: 16.9.0 diff --git a/scripts/generate-examples-derivatives/generate-docs.ts b/scripts/generate-examples-derivatives/generate-docs.ts index 354a7e87a..2fff8d742 100644 --- a/scripts/generate-examples-derivatives/generate-docs.ts +++ b/scripts/generate-examples-derivatives/generate-docs.ts @@ -21,7 +21,7 @@ export const generateDocs = async (examples: Example[]) => { // Delete all existing to handle case of renaming or deleting examples. await deleteFiles({ pattern: `./website/content/examples/*.md`, - options: { ignore: [`./website/content/examples/welcome.md`] }, + options: { ignore: [`./website/content/examples/index.md`] }, }) await Promise.all(examplesTransformed.map(async (example) => { @@ -126,8 +126,7 @@ const transformRewriteGraffleImports = (example: Example) => { ) .replaceAll( `import { Atlas } from './$/generated-clients/atlas/__.js'`, - `// ---cut--- -import { Graffle as Atlas } from './graffle/__.js'`, + `import { Graffle as Atlas } from './graffle/__.js'`, ) .replaceAll( /import ({[^}]+}) from '.\/\$\/generated-clients\/([^/]+)\/__\.js'/g, @@ -162,6 +161,7 @@ const transformRewriteHelperImports = (example: Example) => { .replaceAll(/^import.*\$\/helpers.*$\n/gm, ``) .replaceAll(`documentQueryContinents`, `{ document: \`${documentQueryContinents.document}\` }`) .replaceAll(`publicGraphQLSchemaEndpoints.Atlas`, `\`${publicGraphQLSchemaEndpoints.Atlas}\``) + .replaceAll(/interceptAndShowOutput.*\n/g, ``) .replaceAll(/showJson|show/g, consoleLog) // We disabled this because the popover gets in the way of output below often. // .replaceAll(/(^console.log.*$)/gm, `$1\n//${` `.repeat(consoleLog.length - 1)}^?`) diff --git a/src/entrypoints/extensions.ts b/src/entrypoints/extensions.ts index e03391916..60171f5ca 100644 --- a/src/entrypoints/extensions.ts +++ b/src/entrypoints/extensions.ts @@ -1 +1,2 @@ +export { Opentelemetry } from '../layers/7_extensions/Opentelemetry/Opentelemetry.js' export { Upload } from '../layers/7_extensions/Upload/Upload.js' diff --git a/src/layers/7_extensions/Opentelemetry/Opentelemetry.ts b/src/layers/7_extensions/Opentelemetry/Opentelemetry.ts new file mode 100644 index 000000000..03eb7c793 --- /dev/null +++ b/src/layers/7_extensions/Opentelemetry/Opentelemetry.ts @@ -0,0 +1,33 @@ +import { trace, type Tracer } from '@opentelemetry/api' +import { createExtension } from '../../5_createExtension/createExtension.js' +import { createConfig, type Input } from './config.js' + +const name = `Opentelemetry` + +export const Opentelemetry = (input?: Input) => { + const config = createConfig(input) + const tracer = trace.getTracer(config.tracerName) + const startActiveGraffleSpan = startActiveSpan(tracer) + + return createExtension({ + name, + anyware: async ({ encode }) => { + return await startActiveGraffleSpan(`request`, async () => { + const { pack } = await startActiveGraffleSpan(`encode`, encode) + const { exchange } = await startActiveGraffleSpan(`pack`, pack) + const { unpack } = await startActiveGraffleSpan(`exchange`, exchange) + const { decode } = await startActiveGraffleSpan(`unpack`, unpack) + const result = await startActiveGraffleSpan(`decode`, decode) + return result + }) + }, + }) +} + +const startActiveSpan = (tracer: Tracer) => (name: string, fn: () => Promise): Promise => { + return tracer.startActiveSpan(name, async (span) => { + const result = await fn() + span.end() + return result + }) +} diff --git a/src/layers/7_extensions/Opentelemetry/config.ts b/src/layers/7_extensions/Opentelemetry/config.ts new file mode 100644 index 000000000..d1e952d6a --- /dev/null +++ b/src/layers/7_extensions/Opentelemetry/config.ts @@ -0,0 +1,20 @@ +export type Input = { + /** + * @defaultValue `"opentelemetry"` + */ + tracerName?: string +} + +export type Config = { + tracerName: string +} + +export const defaults = { + tracerName: `graffle`, +} satisfies Config + +export const createConfig = (input?: Input): Config => { + return { + tracerName: input?.tracerName ?? defaults.tracerName, + } +} diff --git a/tests/examples/extension|extension_opentelemetry.test.ts b/tests/examples/extension|extension_opentelemetry.test.ts new file mode 100644 index 000000000..c71c7d688 --- /dev/null +++ b/tests/examples/extension|extension_opentelemetry.test.ts @@ -0,0 +1,20 @@ +// @vitest-environment node + +// WARNING: +// This test is generated by scripts/generate-example-derivatives/generate.ts +// Do not modify this file directly. + +import { expect, test } from 'vitest' +import { encode } from '../../examples/__outputs__/extension|extension_opentelemetry.output.encoder.js' +import { runExample } from '../../scripts/generate-examples-derivatives/helpers.js' + +test(`extension|extension_opentelemetry`, async () => { + const exampleResult = await runExample(`./examples/extension|extension_opentelemetry.ts`) + // Examples should output their data results. + const exampleResultMaybeEncoded = encode(exampleResult) + // If ever outputs vary by Node version, you can use this to snapshot by Node version. + // const nodeMajor = process.version.match(/v(\d+)/)?.[1] ?? `unknown` + await expect(exampleResultMaybeEncoded).toMatchFileSnapshot( + `../.././examples/__outputs__/extension|extension_opentelemetry.output.test.txt`, + ) +}) diff --git a/website/.vitepress/config.ts b/website/.vitepress/config.ts index bded1351c..fa3dc5305 100644 --- a/website/.vitepress/config.ts +++ b/website/.vitepress/config.ts @@ -147,8 +147,8 @@ export default defineConfig({ text: 'Extensions', collapsed: false, items: [ + { text: 'Opentelemetry', link: '/guides/extensions/opentelemetry' }, { text: 'File Upload', link: '/guides/extensions/file-upload' }, - { text: 'OTEL', link: '/guides/extensions/otel' }, { text: 'Or Throw', link: '/guides/extensions/or-throw' }, ], }, diff --git a/website/.vitepress/configExamples.ts b/website/.vitepress/configExamples.ts index 661a5e312..780fe6cc0 100644 --- a/website/.vitepress/configExamples.ts +++ b/website/.vitepress/configExamples.ts @@ -2,6 +2,15 @@ import { DefaultTheme } from 'vitepress' export const sidebarExamples:DefaultTheme.SidebarItem[] = [ + { + "text": "Extension", + "items": [ + { + "text": "Opentelemetry", + "link": "/examples/extension-opentelemetry" + } + ] + }, { "text": "Generated", "items": [ diff --git a/website/content/examples/extension-opentelemetry.md b/website/content/examples/extension-opentelemetry.md new file mode 100644 index 000000000..918d80c73 --- /dev/null +++ b/website/content/examples/extension-opentelemetry.md @@ -0,0 +1,208 @@ +--- +aside: false +--- + +# Opentelemetry + + +```ts twoslash +import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base' +import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node' +import { Opentelemetry } from 'graffle/extensions' +import { Graffle as Atlas } from './graffle/__.js' + + +// Setup Opentelemetry +// 1. Initialize the OpenTelemetry provider +// 2. Register the provider to make the OpenTelemetry API use it +const exporter = new ConsoleSpanExporter() +const processor = new SimpleSpanProcessor(exporter) +const provider = new NodeTracerProvider() +provider.addSpanProcessor(processor) +provider.register() + +const graffle = Atlas.create().use(Opentelemetry()) +const result = await graffle.rawString({ document: `query { continents { name } }` }) +console.log(result.data) +``` + + +#### Outputs + + +```txt +{ + resource: { + attributes: { + 'service.name': 'unknown_service:/Users/jasonkuhrt/Library/pnpm/nodejs/22.7.0/bin/node', + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': '1.26.0' + } + }, + instrumentationScope: { name: 'graffle', version: undefined, schemaUrl: undefined }, + traceId: '93ece70aa164958fd1b3d3c6c89e9f70', + parentId: 'e3b3fd57f531674b', + traceState: undefined, + name: 'encode', + id: '0f8e4e2d2fa7a1d1', + kind: 0, + timestamp: 1726068743834000, + duration: 442.792, + attributes: {}, + status: { code: 0 }, + events: [], + links: [] +} +``` + + +```txt +{ + resource: { + attributes: { + 'service.name': 'unknown_service:/Users/jasonkuhrt/Library/pnpm/nodejs/22.7.0/bin/node', + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': '1.26.0' + } + }, + instrumentationScope: { name: 'graffle', version: undefined, schemaUrl: undefined }, + traceId: '93ece70aa164958fd1b3d3c6c89e9f70', + parentId: 'e3b3fd57f531674b', + traceState: undefined, + name: 'pack', + id: '37766e2e0fa6ea2e', + kind: 0, + timestamp: 1726068743836000, + duration: 808.5, + attributes: {}, + status: { code: 0 }, + events: [], + links: [] +} +``` + + +```txt +{ + resource: { + attributes: { + 'service.name': 'unknown_service:/Users/jasonkuhrt/Library/pnpm/nodejs/22.7.0/bin/node', + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': '1.26.0' + } + }, + instrumentationScope: { name: 'graffle', version: undefined, schemaUrl: undefined }, + traceId: '93ece70aa164958fd1b3d3c6c89e9f70', + parentId: 'e3b3fd57f531674b', + traceState: undefined, + name: 'exchange', + id: 'ed9ae7aad6fd1e69', + kind: 0, + timestamp: 1726068743837000, + duration: 329989.458, + attributes: {}, + status: { code: 0 }, + events: [], + links: [] +} +``` + + +```txt +{ + resource: { + attributes: { + 'service.name': 'unknown_service:/Users/jasonkuhrt/Library/pnpm/nodejs/22.7.0/bin/node', + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': '1.26.0' + } + }, + instrumentationScope: { name: 'graffle', version: undefined, schemaUrl: undefined }, + traceId: '93ece70aa164958fd1b3d3c6c89e9f70', + parentId: 'e3b3fd57f531674b', + traceState: undefined, + name: 'unpack', + id: 'd0d9cbd74e358490', + kind: 0, + timestamp: 1726068744168000, + duration: 1907.291, + attributes: {}, + status: { code: 0 }, + events: [], + links: [] +} +``` + + +```txt +{ + resource: { + attributes: { + 'service.name': 'unknown_service:/Users/jasonkuhrt/Library/pnpm/nodejs/22.7.0/bin/node', + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': '1.26.0' + } + }, + instrumentationScope: { name: 'graffle', version: undefined, schemaUrl: undefined }, + traceId: '93ece70aa164958fd1b3d3c6c89e9f70', + parentId: 'e3b3fd57f531674b', + traceState: undefined, + name: 'decode', + id: '51a5859eae82dd62', + kind: 0, + timestamp: 1726068744170000, + duration: 97.958, + attributes: {}, + status: { code: 0 }, + events: [], + links: [] +} +``` + + +```txt +{ + resource: { + attributes: { + 'service.name': 'unknown_service:/Users/jasonkuhrt/Library/pnpm/nodejs/22.7.0/bin/node', + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': '1.26.0' + } + }, + instrumentationScope: { name: 'graffle', version: undefined, schemaUrl: undefined }, + traceId: '93ece70aa164958fd1b3d3c6c89e9f70', + parentId: undefined, + traceState: undefined, + name: 'request', + id: 'e3b3fd57f531674b', + kind: 0, + timestamp: 1726068743834000, + duration: 336135.125, + attributes: {}, + status: { code: 0 }, + events: [], + links: [] +} +``` + + +```txt +{ + continents: [ + { name: 'Africa' }, + { name: 'Antarctica' }, + { name: 'Asia' }, + { name: 'Europe' }, + { name: 'North America' }, + { name: 'Oceania' }, + { name: 'South America' } + ] +} +``` + diff --git a/website/content/examples/generated-arguments.md b/website/content/examples/generated-arguments.md index b54ba3bb5..ed8c87952 100644 --- a/website/content/examples/generated-arguments.md +++ b/website/content/examples/generated-arguments.md @@ -8,7 +8,6 @@ This example shows how to use the TypeScript interface for GraphQL arguments. ```ts twoslash -// ---cut--- import { Graffle as Atlas } from './graffle/__.js' const atlas = Atlas.create() diff --git a/website/content/examples/output-default.md b/website/content/examples/output-default.md index c6673e7aa..da0c8ec2b 100644 --- a/website/content/examples/output-default.md +++ b/website/content/examples/output-default.md @@ -8,7 +8,6 @@ This example shows the default output behavior. ```ts twoslash -// ---cut--- import { Graffle as Atlas } from './graffle/__.js' const atlas = Atlas.create() diff --git a/website/content/examples/output-envelope-error-throw.md b/website/content/examples/output-envelope-error-throw.md index 5c874cdfa..cb8235993 100644 --- a/website/content/examples/output-envelope-error-throw.md +++ b/website/content/examples/output-envelope-error-throw.md @@ -8,7 +8,6 @@ This example shows how to configure output to throw errors even when using the e ```ts twoslash -// ---cut--- import { Graffle as Atlas } from './graffle/__.js' const atlas = Atlas.create({ diff --git a/website/content/examples/output-envelope-error.md b/website/content/examples/output-envelope-error.md index f744c3dee..c7dbc7949 100644 --- a/website/content/examples/output-envelope-error.md +++ b/website/content/examples/output-envelope-error.md @@ -8,7 +8,6 @@ This example shows how to configure output to embed errors into the envelope. ```ts twoslash -// ---cut--- import { Graffle as Atlas } from './graffle/__.js' const atlas = Atlas diff --git a/website/content/examples/output-envelope.md b/website/content/examples/output-envelope.md index 6267d69df..af7151898 100644 --- a/website/content/examples/output-envelope.md +++ b/website/content/examples/output-envelope.md @@ -8,7 +8,6 @@ This example shows how to configure output to use the envelope. ```ts twoslash -// ---cut--- import { Graffle as Atlas } from './graffle/__.js' const atlas = Atlas.create({ @@ -47,7 +46,7 @@ console.log(result) headers: Headers { connection: 'keep-alive', 'content-length': '119', - 'x-served-by': 'cache-yul1970023-YUL', + 'x-served-by': 'cache-yul1970045-YUL', 'accept-ranges': 'bytes', date: 'Sun, 08 Sep 2024 18:13:26 GMT', 'content-type': 'application/graphql-response+json; charset=utf-8', @@ -60,13 +59,13 @@ console.log(result) 'alt-svc': 'h3=":443"; ma=86400', 'access-control-allow-origin': '*', 'x-powered-by': 'Stellate', - age: '185021', + age: '249539', 'cache-control': 'public, s-maxage=2628000, stale-while-revalidate=2628000', 'x-cache': 'HIT', - 'x-cache-hits': '38', + 'x-cache-hits': '5', 'gcdn-cache': 'HIT', - 'stellate-rate-limit-budget-remaining': '36', - 'stellate-rate-limit-rules': '"IP limit";type="RequestCount";budget=50;limited=?0;remaining=36;refill=6', + 'stellate-rate-limit-budget-remaining': '41', + 'stellate-rate-limit-rules': '"IP limit";type="RequestCount";budget=50;limited=?0;remaining=41;refill=60', 'stellate-rate-limit-decision': 'pass', 'stellate-rate-limit-budget-required': '5', 'content-encoding': 'br' diff --git a/website/content/examples/output-return-error.md b/website/content/examples/output-return-error.md index 5943b9f2f..b691aa6ff 100644 --- a/website/content/examples/output-return-error.md +++ b/website/content/examples/output-return-error.md @@ -8,7 +8,6 @@ This example shows how to configure output to have errors returned instead of e. ```ts twoslash -// ---cut--- import { Graffle as Atlas } from './graffle/__.js' const atlas = Atlas diff --git a/website/content/examples/transport-http-dynamic-headers.md b/website/content/examples/transport-http-dynamic-headers.md index 66136990b..4d369c058 100644 --- a/website/content/examples/transport-http-dynamic-headers.md +++ b/website/content/examples/transport-http-dynamic-headers.md @@ -43,7 +43,7 @@ await graffle.rawString({ document: `{ languages { code } }` }) headers: Headers { accept: 'application/graphql-response+json; charset=utf-8, application/json; charset=utf-8', 'content-type': 'application/json', - 'x-sent-at-time': '1726004226733' + 'x-sent-at-time': '1726068743905' }, signal: undefined, method: 'post', diff --git a/website/content/examples/transport-http-method-get.md b/website/content/examples/transport-http-method-get.md index 1888bc8cd..946112c32 100644 --- a/website/content/examples/transport-http-method-get.md +++ b/website/content/examples/transport-http-method-get.md @@ -45,14 +45,14 @@ await graffle.rawString({ document: `query { pokemonByName(name:"Nano") { hp } } signal: undefined, method: 'post', url: URL { - href: 'http://localhost:3000/graphql', - origin: 'http://localhost:3000', + href: 'http://localhost:3001/graphql', + origin: 'http://localhost:3001', protocol: 'http:', username: '', password: '', - host: 'localhost:3000', + host: 'localhost:3001', hostname: 'localhost', - port: '3000', + port: '3001', pathname: '/graphql', search: '', searchParams: URLSearchParams {}, @@ -72,14 +72,14 @@ await graffle.rawString({ document: `query { pokemonByName(name:"Nano") { hp } } signal: undefined, method: 'get', url: URL { - href: 'http://localhost:3000/graphql?query=query+%7B+pokemonByName%28name%3A%22Nano%22%29+%7B+hp+%7D+%7D', - origin: 'http://localhost:3000', + href: 'http://localhost:3001/graphql?query=query+%7B+pokemonByName%28name%3A%22Nano%22%29+%7B+hp+%7D+%7D', + origin: 'http://localhost:3001', protocol: 'http:', username: '', password: '', - host: 'localhost:3000', + host: 'localhost:3001', hostname: 'localhost', - port: '3000', + port: '3001', pathname: '/graphql', search: '?query=query+%7B+pokemonByName%28name%3A%22Nano%22%29+%7B+hp+%7D+%7D', searchParams: URLSearchParams { 'query' => 'query { pokemonByName(name:"Nano") { hp } }' }, diff --git a/website/content/guides/_example_links/extension.md b/website/content/guides/_example_links/extension.md index 4f6535c57..f198f988f 100644 --- a/website/content/guides/_example_links/extension.md +++ b/website/content/guides/_example_links/extension.md @@ -1 +1 @@ -###### Examples -> [Custom Fetch](../../examples/transport-http-custom-fetch.md) / [Dynamic Headers](../../examples/transport-http-dynamic-headers.md) +###### Examples -> [Opentelemetry](../../examples/extension-opentelemetry.md) / [Custom Fetch](../../examples/transport-http-custom-fetch.md) / [Dynamic Headers](../../examples/transport-http-dynamic-headers.md) diff --git a/website/content/guides/_example_links/extension_opentelemetry.md b/website/content/guides/_example_links/extension_opentelemetry.md new file mode 100644 index 000000000..a361ac7d1 --- /dev/null +++ b/website/content/guides/_example_links/extension_opentelemetry.md @@ -0,0 +1 @@ +###### Examples -> [Opentelemetry](../../examples/extension-opentelemetry.md) diff --git a/website/content/guides/_example_links/opentelemetry.md b/website/content/guides/_example_links/opentelemetry.md new file mode 100644 index 000000000..a361ac7d1 --- /dev/null +++ b/website/content/guides/_example_links/opentelemetry.md @@ -0,0 +1 @@ +###### Examples -> [Opentelemetry](../../examples/extension-opentelemetry.md) diff --git a/website/content/guides/extensions/opentelemetry.md b/website/content/guides/extensions/opentelemetry.md new file mode 100644 index 000000000..3efe3b327 --- /dev/null +++ b/website/content/guides/extensions/opentelemetry.md @@ -0,0 +1,36 @@ +# Opentelemetry + + + +You can Instrument requests from Graffle with [OpenTelemetry](https://opentelemetry.io) using the `Opentelemetry` extension. Check out the example to get started. You'll also find output there that shows each span created, allowing you to see its parent, attributes, etc. + +## Dependencies + +Graffle has an optional peer-dependency on `@opentelemetry/api`. To use this extension you'll need to install a compatible version into your project. You'll most likely need a handful of other `@opentelemetry/*` dependencies too. Check out the example for a working demo and some of what those deps might be. + +```sh +pnpm add @opentelemetry/api +``` + +## Span Structure + +Each request executed has a span created named `request` under a tracer called (by default) `graffle`. Within the request there is a span for each [hook](/todo). + +``` +|- request + |- encode + |- pack + |- exchange + |- unpack + |- decode +``` + +Unless you know what you're doing, make sure to **use the Opentelemetry before all other extensions**. Any other extensions used before it will not have their work recorded in the OpenTelemetry span durations which is probably not want you want. + +## Attributes + +Currently no special attributes are written to spans. This could change. We'd love to hear feature requests from you about this. + +## Errors + +Currently there is no special error recording on spans should a hook fail. This will likely change in the future (PRs always welcome!). diff --git a/website/content/guides/extensions/otel.md b/website/content/guides/extensions/otel.md deleted file mode 100644 index 3d79b79df..000000000 --- a/website/content/guides/extensions/otel.md +++ /dev/null @@ -1 +0,0 @@ -# OTEL