279 lines
5.6 KiB
TypeScript
279 lines
5.6 KiB
TypeScript
import {
|
|
Elysia,
|
|
type MapResponse,
|
|
type Context,
|
|
type TraceEvent,
|
|
type TraceProcess
|
|
} from 'elysia'
|
|
|
|
type MaybePromise<T> = T | Promise<T>
|
|
|
|
class TimeLogger {
|
|
private startTimes: Map<string, number>
|
|
private durations: Map<string, number>
|
|
private totalStartTime: number | null
|
|
|
|
constructor() {
|
|
this.startTimes = new Map()
|
|
this.durations = new Map()
|
|
this.totalStartTime = null
|
|
}
|
|
|
|
startTime(name: string) {
|
|
this.startTimes.set(name, performance.now())
|
|
}
|
|
|
|
endTime(name: string) {
|
|
const startTime = this.startTimes.get(name)
|
|
if (startTime !== undefined) {
|
|
const duration = performance.now() - startTime
|
|
this.durations.set(name, duration)
|
|
this.startTimes.delete(name)
|
|
}
|
|
}
|
|
|
|
getCompletedDurations() {
|
|
return Array.from(this.durations.entries()).map(([name, duration]) => ({
|
|
name,
|
|
duration
|
|
}))
|
|
}
|
|
|
|
startTotal(): void {
|
|
this.totalStartTime = performance.now()
|
|
}
|
|
|
|
endTotal(): number | null {
|
|
if (this.totalStartTime === null) return null
|
|
return performance.now() - this.totalStartTime
|
|
}
|
|
}
|
|
|
|
export interface ServerTimingOptions {
|
|
/**
|
|
* Should Elysia report data back to client via 'Server-Sent-Event'
|
|
*/
|
|
report?: boolean
|
|
/**
|
|
* Allow Server Timing to log specified life-cycle events
|
|
*/
|
|
trace?: {
|
|
/**
|
|
* Capture duration from request
|
|
*
|
|
* @default true
|
|
*/
|
|
request?: boolean
|
|
/**
|
|
* Capture duration from parse
|
|
*
|
|
* @default true
|
|
*/
|
|
parse?: boolean
|
|
/**
|
|
* Capture duration from transform
|
|
*
|
|
* @default true
|
|
*/
|
|
transform?: boolean
|
|
/**
|
|
* Capture duration from beforeHandle
|
|
*
|
|
* @default true
|
|
*/
|
|
beforeHandle?: boolean
|
|
/**
|
|
* Capture duration from handle
|
|
*
|
|
* @default true
|
|
*/
|
|
handle?: boolean
|
|
/**
|
|
* Capture duration from afterHandle
|
|
*
|
|
* @default true
|
|
*/
|
|
afterHandle?: boolean
|
|
/**
|
|
* Capture duration from mapResponse
|
|
*
|
|
* @default true
|
|
*/
|
|
error?: boolean
|
|
/**
|
|
* Capture duration from mapResponse
|
|
*
|
|
* @default true
|
|
*/
|
|
mapResponse?: boolean
|
|
/**
|
|
* Capture total duration from start to finish
|
|
*
|
|
* @default true
|
|
*/
|
|
total?: boolean
|
|
}
|
|
/**
|
|
* Determine whether or not Server Timing should be enabled
|
|
*
|
|
* @default NODE_ENV !== 'production'
|
|
*/
|
|
enabled?: boolean
|
|
/**
|
|
* A condition whether server timing should be log
|
|
*
|
|
* @default undefined
|
|
*/
|
|
allow?:
|
|
| MaybePromise<boolean>
|
|
| ((context: Omit<Context, 'path'>) => MaybePromise<boolean>)
|
|
/**
|
|
* A custom mapResponse provided by user
|
|
*
|
|
* @default undefined
|
|
*/
|
|
mapResponse?: MapResponse
|
|
}
|
|
|
|
const getLabel = (
|
|
event: TraceEvent,
|
|
listener: (
|
|
callback: (process: TraceProcess<'begin', true>) => unknown
|
|
) => unknown,
|
|
write: (value: string) => void
|
|
) => {
|
|
listener(async ({ onStop, onEvent, total }) => {
|
|
let label = ''
|
|
|
|
if (total === 0) return
|
|
|
|
onEvent(({ name, index, onStop }) => {
|
|
onStop(({ elapsed }) => {
|
|
label += `${event}.${index}.${name || 'anon'};dur=${elapsed},`
|
|
})
|
|
})
|
|
|
|
onStop(({ elapsed }) => {
|
|
label += `${event};dur=${elapsed},`
|
|
|
|
write(label)
|
|
})
|
|
})
|
|
}
|
|
|
|
export const serverTiming = ({
|
|
allow,
|
|
enabled = process.env.NODE_ENV !== 'production',
|
|
trace: {
|
|
request: traceRequest = true,
|
|
parse: traceParse = true,
|
|
transform: traceTransform = true,
|
|
beforeHandle: traceBeforeHandle = true,
|
|
handle: traceHandle = true,
|
|
afterHandle: traceAfterHandle = true,
|
|
error: traceError = true,
|
|
mapResponse: traceMapResponse = true,
|
|
total: traceTotal = true
|
|
} = {},
|
|
mapResponse
|
|
}: ServerTimingOptions = {}) => {
|
|
const app = new Elysia().decorate('timeLog', new TimeLogger()).trace(
|
|
{ as: 'global' },
|
|
async ({
|
|
onRequest,
|
|
onParse,
|
|
onTransform,
|
|
onBeforeHandle,
|
|
onHandle,
|
|
onAfterHandle,
|
|
onMapResponse,
|
|
onError,
|
|
set,
|
|
context,
|
|
response,
|
|
context: {
|
|
request: { method }
|
|
}
|
|
}) => {
|
|
|
|
if (!enabled) return
|
|
let label = ''
|
|
|
|
const write = (nextValue: string) => {
|
|
label += nextValue
|
|
}
|
|
|
|
let start: number
|
|
|
|
onRequest(({ begin }) => {
|
|
context.timeLog.startTotal()
|
|
start = begin
|
|
})
|
|
|
|
if (traceRequest) getLabel('request', onRequest, write)
|
|
if (traceParse) getLabel('parse', onParse, write)
|
|
if (traceTransform) getLabel('transform', onTransform, write)
|
|
if (traceBeforeHandle)
|
|
getLabel('beforeHandle', onBeforeHandle, write)
|
|
if (traceAfterHandle) getLabel('afterHandle', onAfterHandle, write)
|
|
if (traceError) getLabel('error', onError, write)
|
|
if (traceMapResponse) getLabel('mapResponse', onMapResponse, write)
|
|
|
|
if (traceHandle)
|
|
onHandle(({ name, onStop }) => {
|
|
onStop(({ elapsed }) => {
|
|
label += `handle.${name};dur=${elapsed},`
|
|
})
|
|
})
|
|
|
|
onMapResponse(({ onStop }) => {
|
|
onStop(async ({ end }) => {
|
|
const completedDurations =
|
|
context.timeLog.getCompletedDurations()
|
|
if (completedDurations.length > 0) {
|
|
label +=
|
|
completedDurations
|
|
.map(
|
|
({ name, duration }) =>
|
|
`${name};dur=${duration}`
|
|
)
|
|
.join(', ') + ','
|
|
}
|
|
const elapsed = context.timeLog.endTotal();
|
|
|
|
let allowed = allow
|
|
if (allowed instanceof Promise) allowed = await allowed
|
|
|
|
if (traceTotal) label += `total;dur=${elapsed}`
|
|
else label = label.slice(0, -1)
|
|
|
|
// ? Must wait until request is reported
|
|
switch (typeof allowed) {
|
|
case 'boolean':
|
|
if (allowed === false)
|
|
delete set.headers['Server-Timing']
|
|
|
|
set.headers['Server-Timing'] = label
|
|
|
|
break
|
|
|
|
case 'function':
|
|
if ((await allowed(context)) === false)
|
|
delete set.headers['Server-Timing']
|
|
|
|
set.headers['Server-Timing'] = label
|
|
|
|
break
|
|
|
|
default:
|
|
set.headers['Server-Timing'] = label
|
|
}
|
|
})
|
|
})
|
|
}
|
|
)
|
|
return app
|
|
}
|
|
|
|
export default serverTiming
|