Skip to content

Commit a298856

Browse files
authored
feat: server times (#532)
1 parent 029d0f5 commit a298856

20 files changed

+300
-60
lines changed

src/app.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,9 @@ const build = (opts: buildOpts = {}): FastifyInstance => {
5252
app.addSchema(schemas.errorSchema)
5353

5454
app.register(plugins.tenantId)
55-
app.register(plugins.metrics({ enabledEndpoint: !isMultitenant }))
5655
app.register(plugins.logTenantId)
56+
app.register(plugins.metrics({ enabledEndpoint: !isMultitenant }))
57+
app.register(plugins.tracing)
5758
app.register(plugins.logRequest({ excludeUrls: ['/status', '/metrics', '/health'] }))
5859
app.register(routes.tus, { prefix: 'upload/resumable' })
5960
app.register(routes.bucket, { prefix: 'bucket' })

src/config.ts

+16-5
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,10 @@ type StorageConfigType = {
112112
s3ProtocolAccessKeyId?: string
113113
s3ProtocolAccessKeySecret?: string
114114
s3ProtocolNonCanonicalHostHeader?: string
115+
tracingEnabled?: boolean
115116
tracingMode?: string
117+
tracingTimeMinDuration: number
118+
tracingReturnServerTimings: boolean
116119
}
117120

118121
function getOptionalConfigFromEnv(key: string, fallback?: string): string | undefined {
@@ -160,16 +163,18 @@ export function getConfig(options?: { reload?: boolean }): StorageConfigType {
160163
}
161164

162165
envPaths.map((envPath) => dotenv.config({ path: envPath, override: false }))
166+
const isMultitenant = getOptionalConfigFromEnv('MULTI_TENANT', 'IS_MULTITENANT') === 'true'
163167

164168
config = {
165169
isProduction: process.env.NODE_ENV === 'production',
166170
exposeDocs: getOptionalConfigFromEnv('EXPOSE_DOCS') !== 'false',
171+
isMultitenant,
167172
// Tenant
168-
tenantId:
169-
getOptionalConfigFromEnv('PROJECT_REF') ||
170-
getOptionalConfigFromEnv('TENANT_ID') ||
171-
'storage-single-tenant',
172-
isMultitenant: getOptionalConfigFromEnv('MULTI_TENANT', 'IS_MULTITENANT') === 'true',
173+
tenantId: isMultitenant
174+
? ''
175+
: getOptionalConfigFromEnv('PROJECT_REF') ||
176+
getOptionalConfigFromEnv('TENANT_ID') ||
177+
'storage-single-tenant',
173178

174179
// Server
175180
region: getOptionalConfigFromEnv('SERVER_REGION', 'REGION') || 'not-specified',
@@ -312,7 +317,13 @@ export function getConfig(options?: { reload?: boolean }): StorageConfigType {
312317
defaultMetricsEnabled: !(
313318
getOptionalConfigFromEnv('DEFAULT_METRICS_ENABLED', 'ENABLE_DEFAULT_METRICS') === 'false'
314319
),
320+
tracingEnabled: getOptionalConfigFromEnv('TRACING_ENABLED') === 'true',
315321
tracingMode: getOptionalConfigFromEnv('TRACING_MODE') ?? 'basic',
322+
tracingTimeMinDuration: parseFloat(
323+
getOptionalConfigFromEnv('TRACING_SERVER_TIME_MIN_DURATION') ?? '100.0'
324+
),
325+
tracingReturnServerTimings:
326+
getOptionalConfigFromEnv('TRACING_RETURN_SERVER_TIMINGS') === 'true',
316327

317328
// Queue
318329
pgQueueEnable: getOptionalConfigFromEnv('PG_QUEUE_ENABLE', 'ENABLE_QUEUE_EVENTS') === 'true',

src/http/plugins/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ export * from './tenant-feature'
99
export * from './metrics'
1010
export * from './xml'
1111
export * from './signature-v4'
12-
export * from './tracing-mode'
12+
export * from './tracing'

src/http/plugins/log-request.ts

+2
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export const logRequest = (options: RequestLoggerOptions) =>
7979
owner: req.owner,
8080
operation: req.operation?.type ?? req.routeConfig.operation?.type,
8181
resources: req.resources,
82+
serverTimes: req.serverTimings,
8283
})
8384
})
8485

@@ -112,6 +113,7 @@ export const logRequest = (options: RequestLoggerOptions) =>
112113
owner: req.owner,
113114
resources: req.resources,
114115
operation: req.operation?.type ?? req.routeConfig.operation?.type,
116+
serverTimes: req.serverTimings,
115117
})
116118
})
117119
})

src/http/plugins/tracing-mode.ts

-23
This file was deleted.

src/http/plugins/tracing.ts

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import fastifyPlugin from 'fastify-plugin'
2+
import { isIP } from 'net'
3+
import { getTenantConfig } from '@internal/database'
4+
5+
import { getConfig } from '../../config'
6+
import { context, trace } from '@opentelemetry/api'
7+
import { traceCollector } from '@internal/monitoring/otel-processor'
8+
import { ReadableSpan } from '@opentelemetry/sdk-trace-base'
9+
import { logger, logSchema } from '@internal/monitoring'
10+
11+
declare module 'fastify' {
12+
interface FastifyRequest {
13+
tracingMode?: string
14+
serverTimings?: { spanName: string; duration: number }[]
15+
}
16+
}
17+
18+
const {
19+
isMultitenant,
20+
tracingEnabled,
21+
tracingMode: defaultTracingMode,
22+
tracingReturnServerTimings,
23+
} = getConfig()
24+
25+
export const tracing = fastifyPlugin(async function tracingMode(fastify) {
26+
if (!tracingEnabled) {
27+
return
28+
}
29+
fastify.register(traceServerTime)
30+
31+
fastify.addHook('onRequest', async (request) => {
32+
if (isMultitenant && request.tenantId) {
33+
const tenantConfig = await getTenantConfig(request.tenantId)
34+
request.tracingMode = tenantConfig.tracingMode
35+
} else {
36+
request.tracingMode = defaultTracingMode
37+
}
38+
})
39+
})
40+
41+
export const traceServerTime = fastifyPlugin(async function traceServerTime(fastify) {
42+
if (!tracingEnabled) {
43+
return
44+
}
45+
fastify.addHook('onResponse', async (request, reply) => {
46+
const traceId = trace.getSpan(context.active())?.spanContext().traceId
47+
48+
if (traceId) {
49+
const spans = traceCollector.getSpansForTrace(traceId)
50+
if (spans) {
51+
try {
52+
const serverTimingHeaders = spansToServerTimings(spans)
53+
54+
request.serverTimings = serverTimingHeaders
55+
56+
// Return Server-Timing if enabled
57+
if (tracingReturnServerTimings) {
58+
const httpServerTimes = serverTimingHeaders
59+
.map(({ spanName, duration }) => {
60+
return `${spanName};dur=${duration.toFixed(3)}` // Convert to milliseconds
61+
})
62+
.join(',')
63+
reply.header('Server-Timing', httpServerTimes)
64+
}
65+
} catch (e) {
66+
logSchema.error(logger, 'failed parsing server times', { error: e, type: 'otel' })
67+
}
68+
69+
traceCollector.clearTrace(traceId)
70+
}
71+
}
72+
})
73+
74+
fastify.addHook('onRequestAbort', async (req) => {
75+
const traceId = trace.getSpan(context.active())?.spanContext().traceId
76+
77+
if (traceId) {
78+
const spans = traceCollector.getSpansForTrace(traceId)
79+
if (spans) {
80+
req.serverTimings = spansToServerTimings(spans)
81+
}
82+
traceCollector.clearTrace(traceId)
83+
}
84+
})
85+
})
86+
87+
function enrichSpanName(spanName: string, span: ReadableSpan) {
88+
if (span.attributes['knex.version']) {
89+
const queryOperation = (span.attributes['db.operation'] as string)?.split(' ').shift()
90+
return (
91+
`pg_query_` +
92+
queryOperation?.toUpperCase() +
93+
(span.attributes['db.sql.table'] ? '_' + span.attributes['db.sql.table'] : '_postgres')
94+
)
95+
}
96+
97+
if (['GET', 'PUT', 'HEAD', 'DELETE', 'POST'].includes(spanName)) {
98+
return `HTTP_${spanName}`
99+
}
100+
101+
return spanName
102+
}
103+
104+
function spansToServerTimings(spans: ReadableSpan[]) {
105+
return spans
106+
.sort((a, b) => {
107+
return a.startTime[1] - b.startTime[1]
108+
})
109+
.map((span) => {
110+
const duration = span.duration[1] // Duration in nanoseconds
111+
112+
let spanName =
113+
span.name
114+
.split('->')
115+
.pop()
116+
?.trimStart()
117+
.replaceAll('\n', '')
118+
.replaceAll('.', '_')
119+
.replaceAll(' ', '_')
120+
.replaceAll('-', '_')
121+
.replaceAll('___', '_')
122+
.replaceAll(':', '_')
123+
.replaceAll('_undefined', '') || 'UNKNOWN'
124+
125+
spanName = enrichSpanName(spanName, span)
126+
const hostName = span.attributes['net.peer.name'] as string | undefined
127+
128+
return {
129+
spanName,
130+
duration: duration / 1e6,
131+
action: span.attributes['db.statement'],
132+
host: hostName
133+
? isIP(hostName)
134+
? hostName
135+
: hostName?.split('.').slice(-3).join('.')
136+
: undefined,
137+
}
138+
})
139+
}

src/http/routes/bucket/getAllBuckets.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export default async function routes(fastify: FastifyInstance) {
4343
'id, name, public, owner, created_at, updated_at, file_size_limit, allowed_mime_types'
4444
)
4545

46-
response.send(results)
46+
return response.send(results)
4747
}
4848
)
4949
}

src/http/routes/bucket/getBucket.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export default async function routes(fastify: FastifyInstance) {
4141
'id, name, owner, public, created_at, updated_at, file_size_limit, allowed_mime_types'
4242
)
4343

44-
response.send(results)
44+
return response.send(results)
4545
}
4646
)
4747
}

src/http/routes/bucket/index.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@ import emptyBucket from './emptyBucket'
55
import getAllBuckets from './getAllBuckets'
66
import getBucket from './getBucket'
77
import updateBucket from './updateBucket'
8-
import { storage, jwt, db, tracingMode } from '../../plugins'
8+
import { storage, jwt, db } from '../../plugins'
99

1010
export default async function routes(fastify: FastifyInstance) {
1111
fastify.register(jwt)
1212
fastify.register(db)
1313
fastify.register(storage)
14-
fastify.register(tracingMode)
1514

1615
fastify.register(createBucket)
1716
fastify.register(emptyBucket)

src/http/routes/object/index.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { FastifyInstance } from 'fastify'
2-
import { jwt, storage, dbSuperUser, db, tracingMode } from '../../plugins'
2+
import { jwt, storage, dbSuperUser, db } from '../../plugins'
33
import copyObject from './copyObject'
44
import createObject from './createObject'
55
import deleteObject from './deleteObject'
@@ -24,7 +24,6 @@ export default async function routes(fastify: FastifyInstance) {
2424
fastify.register(jwt)
2525
fastify.register(db)
2626
fastify.register(storage)
27-
fastify.register(tracingMode)
2827

2928
fastify.register(deleteObject)
3029
fastify.register(deleteObjects)
@@ -43,7 +42,6 @@ export default async function routes(fastify: FastifyInstance) {
4342
fastify.register(async (fastify) => {
4443
fastify.register(dbSuperUser)
4544
fastify.register(storage)
46-
fastify.register(tracingMode)
4745

4846
fastify.register(getPublicObject)
4947
fastify.register(getSignedObject)

src/http/routes/object/listObjects.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export default async function routes(fastify: FastifyInstance) {
7373
},
7474
})
7575

76-
response.status(200).send(results)
76+
return response.status(200).send(results)
7777
}
7878
)
7979
}

src/http/routes/render/index.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { FastifyInstance } from 'fastify'
22
import renderPublicImage from './renderPublicImage'
33
import renderAuthenticatedImage from './renderAuthenticatedImage'
44
import renderSignedImage from './renderSignedImage'
5-
import { jwt, storage, requireTenantFeature, db, dbSuperUser, tracingMode } from '../../plugins'
5+
import { jwt, storage, requireTenantFeature, db, dbSuperUser } from '../../plugins'
66
import { getConfig } from '../../../config'
77
import { rateLimiter } from './rate-limiter'
88

@@ -23,7 +23,6 @@ export default async function routes(fastify: FastifyInstance) {
2323
fastify.register(jwt)
2424
fastify.register(db)
2525
fastify.register(storage)
26-
fastify.register(tracingMode)
2726

2827
fastify.register(renderAuthenticatedImage)
2928
})
@@ -37,7 +36,6 @@ export default async function routes(fastify: FastifyInstance) {
3736

3837
fastify.register(dbSuperUser)
3938
fastify.register(storage)
40-
fastify.register(tracingMode)
4139

4240
fastify.register(renderSignedImage)
4341
fastify.register(renderPublicImage)

src/http/routes/s3/index.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { FastifyInstance, RouteHandlerMethod } from 'fastify'
22
import { JSONSchema } from 'json-schema-to-ts'
33
import { trace } from '@opentelemetry/api'
4-
import { db, jsonToXml, signatureV4, storage, tracingMode } from '../../plugins'
4+
import { db, jsonToXml, signatureV4, storage } from '../../plugins'
55
import { findArrayPathsInSchemas, getRouter, RequestInput } from './router'
66
import { s3ErrorHandler } from './error-handler'
77

@@ -110,7 +110,6 @@ export default async function routes(fastify: FastifyInstance) {
110110
fastify.register(signatureV4)
111111
fastify.register(db)
112112
fastify.register(storage)
113-
fastify.register(tracingMode)
114113

115114
localFastify[method](
116115
routePath,

0 commit comments

Comments
 (0)