Skip to content

Commit 744a3a3

Browse files
authored
feat: a minimal fetch instrumentation (#383)
* move netlify span exporter to the exporters directory * add a minimal fetch instrumentation * add instrumentations and exporters as entrypoints * remove lies
1 parent ee49e8a commit 744a3a3

File tree

5 files changed

+117
-7
lines changed

5 files changed

+117
-7
lines changed

eslint_temporary_suppressions.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -557,11 +557,9 @@ export default [
557557
},
558558
},
559559
{
560-
files: ['packages/otel/src/bootstrap/netlify_span_exporter.ts'],
560+
files: ['packages/otel/src/exporters/netlify.ts'],
561561
rules: {
562562
'@typescript-eslint/unbound-method': 'off',
563-
'@typescript-eslint/restrict-template-expressions': 'off',
564-
'@typescript-eslint/no-confusing-void-expression': 'off',
565563
},
566564
},
567565
{

packages/otel/src/bootstrap/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export const createTracerProvider = async (options: TracerProviderOptions) => {
3030

3131
const { registerInstrumentations } = await import('@opentelemetry/instrumentation')
3232

33-
const { NetlifySpanExporter } = await import('./netlify_span_exporter.js')
33+
const { NetlifySpanExporter } = await import('../exporters/netlify.js')
3434

3535
const resource = new Resource({
3636
'service.name': options.serviceName,

packages/otel/src/bootstrap/netlify_span_exporter.ts renamed to packages/otel/src/exporters/netlify.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export class NetlifySpanExporter implements SpanExporter {
1818

1919
/** Export spans. */
2020
export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void {
21-
this.#logger.debug(`export ${spans.length} spans`)
21+
this.#logger.debug(`export ${spans.length.toString()} spans`)
2222
if (this.#shutdownOnce.isCalled) {
2323
resultCallback({
2424
code: ExportResultCode.FAILED,
@@ -28,7 +28,7 @@ export class NetlifySpanExporter implements SpanExporter {
2828
}
2929

3030
console.log(TRACE_PREFIX, NetlifySpanExporter.#decoder.decode(JsonTraceSerializer.serializeRequest(spans)))
31-
return resultCallback({ code: ExportResultCode.SUCCESS })
31+
resultCallback({ code: ExportResultCode.SUCCESS })
3232
}
3333

3434
/**
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import * as api from '@opentelemetry/api'
2+
import { InstrumentationConfig, type Instrumentation } from '@opentelemetry/instrumentation'
3+
import { _globalThis } from '@opentelemetry/core'
4+
import { SugaredTracer } from '@opentelemetry/api/experimental'
5+
6+
export interface FetchInstrumentationConfig extends InstrumentationConfig {
7+
getRequestAttributes?(request: Request | RequestInit): api.Attributes
8+
getResponseAttributes?(response: Response): api.Attributes
9+
skipURLs?: string[]
10+
}
11+
12+
export class FetchInstrumentation implements Instrumentation {
13+
instrumentationName = '@netlify/otel/instrumentation-fetch'
14+
instrumentationVersion = '1.0.0'
15+
private originalFetch: typeof fetch | null = null
16+
private config: FetchInstrumentationConfig
17+
private provider?: api.TracerProvider
18+
19+
constructor(config: FetchInstrumentationConfig = {}) {
20+
this.config = config
21+
}
22+
23+
getConfig(): FetchInstrumentationConfig {
24+
return this.config
25+
}
26+
27+
setConfig(): void {}
28+
29+
setMeterProvider(): void {}
30+
setTracerProvider(provider: api.TracerProvider): void {
31+
this.provider = provider
32+
}
33+
getTracerProvider(): api.TracerProvider | undefined {
34+
return this.provider
35+
}
36+
37+
private annotateFromResponse(span: api.Span, response: Response): void {
38+
const extras = this.config.getResponseAttributes?.(response) ?? {}
39+
// these are based on @opentelemetry/semantic-convention 1.36
40+
span.setAttributes({
41+
...extras,
42+
'http.response.status_code': response.status,
43+
...this.prepareHeaders('response', response.headers),
44+
})
45+
}
46+
47+
private annotateFromRequest(span: api.Span, request: Request): void {
48+
const extras = this.config.getRequestAttributes?.(request) ?? {}
49+
const url = new URL(request.url)
50+
// these are based on @opentelemetry/semantic-convention 1.36
51+
span.setAttributes({
52+
...extras,
53+
'http.request.method': request.method,
54+
'url.full': url.href,
55+
'url.host': url.host,
56+
'url.scheme': url.protocol.replace(':', ''),
57+
'server.address': url.hostname,
58+
'server.port': url.port,
59+
...this.prepareHeaders('request', request.headers),
60+
})
61+
}
62+
63+
private prepareHeaders(type: 'request' | 'response', headers: Headers): api.Attributes {
64+
return Object.fromEntries(Array.from(headers.entries()).map(([key, value]) => [`${type}.header.${key}`, value]))
65+
}
66+
67+
private getTracer(): SugaredTracer | undefined {
68+
if (!this.provider) {
69+
return undefined
70+
}
71+
72+
const tracer = this.provider.getTracer(this.instrumentationName, this.instrumentationVersion)
73+
if (tracer instanceof SugaredTracer) {
74+
return tracer
75+
}
76+
77+
return new SugaredTracer(tracer)
78+
}
79+
80+
/**
81+
* patch global fetch
82+
*/
83+
enable(): void {
84+
const originalFetch = _globalThis.fetch
85+
this.originalFetch = originalFetch
86+
_globalThis.fetch = async (resource: RequestInfo | URL, options?: RequestInit): Promise<Response> => {
87+
const url = typeof resource === 'string' ? resource : resource instanceof URL ? resource.href : resource.url
88+
const tracer = this.getTracer()
89+
if (!tracer || this.config.skipURLs?.some((skip) => url.startsWith(skip))) {
90+
return await originalFetch(resource, options)
91+
}
92+
93+
return tracer.withActiveSpan('fetch', async (span) => {
94+
const request = new Request(resource, options)
95+
this.annotateFromRequest(span, request)
96+
const response = await originalFetch(resource, options)
97+
this.annotateFromResponse(span, response)
98+
return response
99+
})
100+
}
101+
}
102+
103+
/**
104+
* unpatch global fetch
105+
*/
106+
disable(): void {
107+
if (this.originalFetch) {
108+
_globalThis.fetch = this.originalFetch
109+
this.originalFetch = null
110+
}
111+
}
112+
}

packages/otel/tsup.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export default defineConfig([
66
{
77
clean: true,
88
format: ['cjs', 'esm'],
9-
entry: ['src/bootstrap/main.ts', 'src/main.ts'],
9+
entry: ['src/bootstrap/main.ts', 'src/main.ts', 'src/exporters/netlify.ts', 'src/instrumentations/fetch.ts'],
1010
tsconfig: 'tsconfig.json',
1111
splitting: false,
1212
bundle: true,

0 commit comments

Comments
 (0)