Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions app/plugins/render-tracer.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { trace } from '@opentelemetry/api'
import { finishRenderSpan, getRenderTrace, startRenderSpan } from '../../utils/render-trace'

export default defineNuxtPlugin({
name: 'comark-render-tracer',
enforce: 'pre',
setup(nuxtApp) {
const event = nuxtApp.ssrContext?.event
if (!event) return

const state = getRenderTrace(event)
if (!state?.render) return

nuxtApp.hook('app:rendered', () => {
finishRenderSpan(state, 'vue')
state.finalize = startRenderSpan(
trace.getTracer('comark-content'),
state.isPayload ? 'nuxt:render:payload' : 'nuxt:render:finalize',
state.render
)
})
},
})
100 changes: 98 additions & 2 deletions server/plugins/tracer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { context, propagation } from '@opentelemetry/api'
import { ensureLocalTracer, localTraceEnabled, shutdownLocalTracer } from '../utils/tracer'
import { context, propagation, SpanStatusCode } from '@opentelemetry/api'
import type { H3Event } from 'h3'
import {
createRenderTrace,
finishRenderSpan,
getRenderTrace,
startRenderSpan,
type RenderTraceState,
} from '../../utils/render-trace'
import { contentTracer, ensureLocalTracer, localTraceEnabled, shutdownLocalTracer } from '../utils/tracer'
import { registerOTel } from '@vercel/otel'

export default defineNitroPlugin((nitro) => {
Expand All @@ -18,4 +26,92 @@ export default defineNitroPlugin((nitro) => {
const requestContext = propagation.extract(context.active(), getRequestHeaders(event))
return context.with(requestContext, () => handler(event))
}

const tracer = contentTracer()
if (!tracer) return

const startRequestTrace = (event: H3Event) => {
const path = event.path
const isPayload = /(?:^|\/)_payload\.json(?:\?|$)/.test(path)
const state = createRenderTrace(event, isPayload)
state.request = startRenderSpan(tracer, 'nitro:request', undefined, {
'http.request.method': event.method,
'url.path': path,
'nuxt.render.payload': isPayload,
})
return state
}

nitro.hooks.hook('request', (event) => {
const accept = getRequestHeader(event, 'accept') || ''
const isPayload = /(?:^|\/)_payload\.json(?:\?|$)/.test(event.path)
if (isPayload || accept.includes('text/html')) startRequestTrace(event)
})

nitro.hooks.hook('render:before', ({ event }) => {
const state = getRenderTrace(event) || startRequestTrace(event)
state.render = startRenderSpan(tracer, 'nuxt:render', state.request, {
'nuxt.render.payload': state.isPayload,
'nitro.cache.enabled': Boolean(event.context.cache),
})
})

nitro.hooks.hook('render:route', ({ canStream, prefersStream }, { event }) => {
const state = getRenderTrace(event)
if (!state?.render) return
state.vue = startRenderSpan(tracer, 'nuxt:render:vue', state.render, {
'nuxt.render.can_stream': canStream,
'nuxt.render.prefers_stream': prefersStream,
})
})

nitro.hooks.hook('render:html', (_html, { event, streaming }) => {
// Streaming calls this hook before Vue renders the body. ISR and payload
// responses are buffered, which gives us the post-render boundary below.
if (streaming) return
const state = getRenderTrace(event)
if (!state?.render) return
finishRenderSpan(state, 'finalize')
state.html = startRenderSpan(tracer, 'nuxt:render:html', state.render)
})

nitro.hooks.hook('render:response', (response, { event }) => {
const state = getRenderTrace(event)
if (!state) return

finishRenderSpan(state, 'vue')
finishRenderSpan(state, 'finalize')
finishRenderSpan(state, 'html')
if (response.statusCode) state.render?.setAttribute('http.response.status_code', response.statusCode)
if (typeof response.body === 'string') {
state.render?.setAttribute('http.response.body.size', new TextEncoder().encode(response.body).byteLength)
}
finishRenderSpan(state, 'render')
state.response = startRenderSpan(tracer, 'nitro:response', state.request)
})

nitro.hooks.hook('afterResponse', (event) => {
const state = getRenderTrace(event)
if (!state) return
finishRenderSpan(state, 'response')
finishRenderSpan(state, 'request')
})

nitro.hooks.hook('error', (error, { event }) => {
if (!event) return
const state = getRenderTrace(event)
if (!state) return
markRenderTraceFailed(state, error)
})
})

function markRenderTraceFailed(state: RenderTraceState, error: unknown): void {
const exception = error instanceof Error ? error : String(error)
for (const key of ['vue', 'finalize', 'html', 'render', 'response', 'request'] as const) {
const span = state[key]
if (!span) continue
span.recordException(exception)
span.setStatus({ code: SpanStatusCode.ERROR })
finishRenderSpan(state, key)
}
}
44 changes: 44 additions & 0 deletions utils/render-trace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { context, trace, type Attributes, type Span, type Tracer } from '@opentelemetry/api'
import type { H3Event } from 'h3'

export interface RenderTraceState {
request?: Span
render?: Span
vue?: Span
finalize?: Span
html?: Span
response?: Span
isPayload: boolean
}

type TracedEventContext = H3Event['context'] & {
_comarkRenderTrace?: RenderTraceState
}

export function getRenderTrace(event: H3Event): RenderTraceState | undefined {
return (event.context as TracedEventContext)._comarkRenderTrace
}

export function createRenderTrace(event: H3Event, isPayload: boolean): RenderTraceState {
const state = { isPayload }
;(event.context as TracedEventContext)._comarkRenderTrace = state
return state
}

export function startRenderSpan(
tracer: Tracer,
name: string,
parent?: Span,
attributes?: Attributes
): Span {
const parentContext = parent ? trace.setSpan(context.active(), parent) : context.active()
return tracer.startSpan(name, { attributes }, parentContext)
}

export function finishRenderSpan(
state: RenderTraceState,
key: Exclude<keyof RenderTraceState, 'isPayload'>
): void {
state[key]?.end()
state[key] = undefined
}