62 lines
2.8 KiB
TypeScript
62 lines
2.8 KiB
TypeScript
import { expect, test, type Page } from '@playwright/test'
|
|
|
|
// Showcase D23: register() in instrumentation.ts starts the OTLP exporters when the platform
|
|
// injects OTEL_EXPORTER_OTLP_ENDPOINT, and every server action logs through shared/api/observe's
|
|
// logger(). This proves the whole chain against a real `next start` production server (not dev,
|
|
// not a mock of register()): a log a server action emits reaches the fake platform's
|
|
// /otel/v1/logs route within the exporter's 5 s batch interval.
|
|
|
|
const platform = 'http://127.0.0.1:4010'
|
|
|
|
interface LogRecordJson {
|
|
body?: { stringValue?: string }
|
|
attributes?: Array<{ key: string; value: { stringValue?: string } }>
|
|
}
|
|
|
|
type OtelState = { otel: { logs: Array<{ authorization: string | null; body: { resourceLogs: Array<{ scopeLogs: Array<{ logRecords: LogRecordJson[] }> }> } }> } }
|
|
|
|
async function otelLogRecords(): Promise<LogRecordJson[]> {
|
|
const response = await fetch(`${platform}/__e2e/state`)
|
|
const state = (await response.json()) as OtelState
|
|
return state.otel.logs.flatMap((post) => post.body.resourceLogs.flatMap((r) => r.scopeLogs.flatMap((s) => s.logRecords)))
|
|
}
|
|
|
|
function attr(record: LogRecordJson, key: string): string | undefined {
|
|
return record.attributes?.find((kv) => kv.key === key)?.value.stringValue
|
|
}
|
|
|
|
async function createNote(page: Page, title: string): Promise<void> {
|
|
await page.goto('/')
|
|
await page.getByLabel('Title').fill(title)
|
|
await page.getByLabel('Body').fill('observability e2e')
|
|
await page.getByRole('button', { name: 'Create note' }).click()
|
|
await expect(page.getByRole('heading', { name: title })).toBeVisible()
|
|
}
|
|
|
|
test('a note.created server action log reaches the OTLP receiver within the batch interval', async ({ page }) => {
|
|
const title = `otlp ${Date.now()}-${Math.random().toString(36).slice(2, 7)}`
|
|
await createNote(page, title)
|
|
|
|
await expect
|
|
.poll(
|
|
async () => (await otelLogRecords()).some((r) => r.body?.stringValue === 'note.created' && attr(r, 'outcome') === 'ok'),
|
|
// BatchExporter's default flush interval is 5 s (exporter.ts); this allows one full
|
|
// interval plus slack for the request round trip and CI scheduling jitter.
|
|
{ timeout: 9_000 },
|
|
)
|
|
.toBe(true)
|
|
})
|
|
|
|
test('the OTLP request carries the deployment token, not the public client', async ({ page }) => {
|
|
const title = `otlp-auth ${Date.now()}-${Math.random().toString(36).slice(2, 7)}`
|
|
await createNote(page, title)
|
|
|
|
await expect
|
|
.poll(async () => (await otelLogRecords()).some((r) => r.body?.stringValue === 'note.created'), { timeout: 9_000 })
|
|
.toBe(true)
|
|
|
|
const response = await fetch(`${platform}/__e2e/state`)
|
|
const state = (await response.json()) as OtelState
|
|
expect(state.otel.logs.every((post) => post.authorization === 'Bearer e2e-deployment-token')).toBe(true)
|
|
})
|