-
Notifications
You must be signed in to change notification settings - Fork 27k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
OpenTelemetry: trace API routes in page router (#62120)
Co-authored-by: JJ Kasper <jj@jjsweb.site>
- Loading branch information
Showing
18 changed files
with
981 additions
and
797 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
6 changes: 6 additions & 0 deletions
6
test/e2e/opentelemetry/app/api/app/[param]/data/edge/route.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export async function GET() { | ||
return new Response(JSON.stringify({ test: 'data-edge' })) | ||
} | ||
|
||
export const runtime = 'edge' | ||
export const dynamic = 'force-dynamic' |
13 changes: 13 additions & 0 deletions
13
test/e2e/opentelemetry/app/app/[param]/rsc-fetch/edge/page.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
// We want to trace this fetch in runtime | ||
export const dynamic = 'force-dynamic' | ||
|
||
export const runtime = 'edge' | ||
|
||
export async function generateMetadata() { | ||
return {} | ||
} | ||
|
||
export default async function Page() { | ||
const data = await fetch('https://vercel.com') | ||
return <pre>RESONSE: {data.status}</pre> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { Server as HttpServer } from 'node:http' | ||
import { SavedSpan } from './constants' | ||
|
||
export interface Collector { | ||
getSpans: () => SavedSpan[] | ||
shutdown: () => Promise<void> | ||
} | ||
|
||
export async function connectCollector({ | ||
port, | ||
}: { | ||
port: number | ||
}): Promise<Collector> { | ||
const spans: SavedSpan[] = [] | ||
|
||
const server = new HttpServer(async (req, res) => { | ||
if (req.method !== 'POST') { | ||
res.writeHead(405) | ||
res.end() | ||
return | ||
} | ||
|
||
const body = await new Promise<Buffer>((resolve, reject) => { | ||
const acc: Buffer[] = [] | ||
req.on('data', (chunk: Buffer) => { | ||
acc.push(chunk) | ||
}) | ||
req.on('end', () => { | ||
resolve(Buffer.concat(acc)) | ||
}) | ||
req.on('error', reject) | ||
}) | ||
|
||
const newSpans = JSON.parse(body.toString('utf-8')) as SavedSpan[] | ||
spans.push(...newSpans) | ||
res.statusCode = 202 | ||
res.end() | ||
}) | ||
|
||
await new Promise<void>((resolve, reject) => { | ||
server.listen(port, (err?: Error) => { | ||
if (err) { | ||
reject(err) | ||
} else { | ||
resolve() | ||
} | ||
}) | ||
}) | ||
|
||
return { | ||
getSpans() { | ||
return spans | ||
}, | ||
shutdown() { | ||
return new Promise<void>((resolve, reject) => | ||
server.close((err) => (err ? reject(err) : resolve())) | ||
).catch((err) => { | ||
console.warn('WARN: collector server disconnect failure:', err) | ||
}) | ||
}, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
if (globalThis.performance === undefined) { | ||
globalThis.performance = { timeOrigin: 0, now: () => Date.now() } as any | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import './instrumentation-polyfill' | ||
|
||
import { Resource } from '@opentelemetry/resources' | ||
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions' | ||
import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base' | ||
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks' | ||
import { | ||
SimpleSpanProcessor, | ||
SpanExporter, | ||
ReadableSpan, | ||
} from '@opentelemetry/sdk-trace-base' | ||
import { | ||
ExportResult, | ||
ExportResultCode, | ||
hrTimeToMicroseconds, | ||
} from '@opentelemetry/core' | ||
|
||
import { SavedSpan } from './constants' | ||
|
||
const serializeSpan = (span: ReadableSpan): SavedSpan => ({ | ||
runtime: process.env.NEXT_RUNTIME, | ||
traceId: span.spanContext().traceId, | ||
parentId: span.parentSpanId, | ||
traceState: span.spanContext().traceState?.serialize(), | ||
name: span.name, | ||
id: span.spanContext().spanId, | ||
kind: span.kind, | ||
timestamp: hrTimeToMicroseconds(span.startTime), | ||
duration: hrTimeToMicroseconds(span.duration), | ||
attributes: span.attributes, | ||
status: span.status, | ||
events: span.events, | ||
links: span.links, | ||
}) | ||
|
||
class TestExporter implements SpanExporter { | ||
constructor(private port: number) {} | ||
|
||
async export( | ||
spans: ReadableSpan[], | ||
resultCallback: (result: ExportResult) => void | ||
) { | ||
try { | ||
const response = await fetch(`http://localhost:${this.port}`, { | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/json', | ||
}, | ||
body: JSON.stringify(spans.map(serializeSpan)), | ||
}) | ||
try { | ||
await response.arrayBuffer() | ||
} catch (e) { | ||
// ignore. | ||
} | ||
if (response.status >= 400) { | ||
console.warn('WARN: TestExporter: response status:', response.status) | ||
resultCallback({ | ||
code: ExportResultCode.FAILED, | ||
error: new Error(`http status ${response.status}`), | ||
}) | ||
} | ||
} catch (e) { | ||
console.warn('WARN: TestExporterP: error:', e) | ||
resultCallback({ code: ExportResultCode.FAILED, error: e }) | ||
} | ||
|
||
resultCallback({ code: ExportResultCode.SUCCESS }) | ||
} | ||
shutdown(): Promise<void> { | ||
return Promise.resolve() | ||
} | ||
} | ||
|
||
export const register = () => { | ||
const contextManager = new AsyncLocalStorageContextManager() | ||
contextManager.enable() | ||
|
||
const provider = new BasicTracerProvider({ | ||
resource: new Resource({ | ||
[SemanticResourceAttributes.SERVICE_NAME]: 'test-next-app', | ||
}), | ||
}) | ||
|
||
if (!process.env.TEST_OTEL_COLLECTOR_PORT) { | ||
throw new Error('TEST_OTEL_COLLECTOR_PORT is not set') | ||
} | ||
const port = parseInt(process.env.TEST_OTEL_COLLECTOR_PORT) | ||
provider.addSpanProcessor(new SimpleSpanProcessor(new TestExporter(port))) | ||
|
||
// Make sure to register you provider | ||
provider.register({ contextManager }) | ||
} |
Oops, something went wrong.