Skip to content

Commit d87675c

Browse files
authored
fix: drop tenant id from metrics to reduce allocations (#1121)
Signed-off-by: ferhat elmas <elmas.ferhat@gmail.com>
1 parent 3f44f1a commit d87675c

9 files changed

Lines changed: 176 additions & 40 deletions

File tree

src/config.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,6 @@ type StorageConfigType = {
187187
upload: boolean
188188
}
189189
prometheusMetricsEnabled: boolean
190-
prometheusMetricsIncludeTenantId: boolean
191190
tenantPoolCacheTtlMs: number
192191
tenantPoolCacheHitLogSampleRate: number
193192
tenantPoolCacheMissLogSampleRate: number
@@ -491,8 +490,6 @@ export function getConfig(options?: { reload?: boolean }): StorageConfigType {
491490

492491
// OpenTelemetry Metrics
493492
prometheusMetricsEnabled: getOptionalConfigFromEnv('PROMETHEUS_METRICS_ENABLED') === 'true',
494-
prometheusMetricsIncludeTenantId:
495-
getOptionalConfigFromEnv('PROMETHEUS_METRICS_INCLUDE_TENANT') === 'true',
496493
otelMetricsEnabled: getOptionalConfigFromEnv('OTEL_METRICS_ENABLED') === 'true',
497494
otelMetricsTemporality: getOptionalConfigFromEnv('OTEL_METRICS_TEMPORALITY') || 'CUMULATIVE',
498495
otelMetricsExportIntervalMs: parseInt(

src/http/plugins/metrics.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import {
2+
httpRequestDuration,
3+
httpRequestSizeBytes,
4+
httpResponseSizeBytes,
5+
} from '@internal/monitoring/metrics'
6+
import Fastify from 'fastify'
7+
import { httpMetrics } from './metrics'
8+
9+
describe('httpMetrics plugin', () => {
10+
it('records HTTP metric attributes without tenant id labels', async () => {
11+
const app = Fastify()
12+
const durationSpy = vi.spyOn(httpRequestDuration, 'record')
13+
const requestSizeSpy = vi.spyOn(httpRequestSizeBytes, 'add')
14+
const responseSizeSpy = vi.spyOn(httpResponseSizeBytes, 'add')
15+
16+
app.decorateRequest('tenantId', 'tenant-a')
17+
await app.register(httpMetrics())
18+
app.post('/objects/:bucket', async (_request, reply) => {
19+
reply.header('content-length', '2')
20+
return 'ok'
21+
})
22+
23+
try {
24+
const response = await app.inject({
25+
method: 'POST',
26+
url: '/objects/bucket-a',
27+
headers: {
28+
'content-length': '7',
29+
'content-type': 'text/plain',
30+
},
31+
payload: 'payload',
32+
})
33+
34+
expect(response.statusCode).toBe(200)
35+
36+
const expectedAttributes = {
37+
method: 'POST',
38+
operation: 'unknown',
39+
status_code: '2xx',
40+
}
41+
42+
expect(durationSpy).toHaveBeenCalledWith(expect.any(Number), expectedAttributes)
43+
expect(requestSizeSpy).toHaveBeenCalledWith(7, expectedAttributes)
44+
expect(responseSizeSpy).toHaveBeenCalledWith(2, expectedAttributes)
45+
} finally {
46+
durationSpy.mockRestore()
47+
requestSizeSpy.mockRestore()
48+
responseSizeSpy.mockRestore()
49+
await app.close()
50+
}
51+
})
52+
})

src/http/plugins/metrics.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@ export const httpMetrics = (options: HttpMetricsOptions = {}) =>
9393
operation:
9494
request.operation?.type || request.routeOptions?.config?.operation?.type || 'unknown',
9595
status_code: statusCode,
96-
tenantId: request.tenantId || '',
9796
}
9897

9998
// Record duration (histogram count replaces httpRequestsTotal)

src/internal/monitoring/metrics.ts

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
import { Attributes, metrics } from '@opentelemetry/api'
2-
import { getConfig } from '../../config'
3-
4-
const { prometheusMetricsIncludeTenantId } = getConfig()
1+
import { metrics } from '@opentelemetry/api'
52

63
// ============================================================================
74
// Metric Registry — tracks all metrics for admin API
@@ -48,35 +45,12 @@ export function isMetricEnabled(name: string): boolean {
4845
// ============================================================================
4946
export const meter = metrics.getMeter('storage-api')
5047

51-
function stripTenantAttrs(attrs: Attributes): Attributes {
52-
const { tenantId, tenant_id, ...rest } = attrs as Record<string, unknown>
53-
return rest as Attributes
54-
}
55-
5648
/**
57-
* Registers a metric in the admin registry and wraps .record()/.add()
58-
* to automatically strip tenant attributes when prometheusMetricsIncludeTenantId is false.
49+
* Registers a metric in the admin registry.
5950
*/
6051
export function registerMetric<T>(name: string, type: MetricType, factory: () => T): T {
6152
metricsRegistry.set(name, { name, type, enabled: !disabledMetrics.has(name) })
62-
const instrument = factory()
63-
64-
if (prometheusMetricsIncludeTenantId) return instrument
65-
66-
// biome-ignore lint/suspicious/noExplicitAny: wrapping OTel instrument methods
67-
const inst = instrument as any
68-
if (typeof inst.record === 'function') {
69-
const original = inst.record.bind(inst)
70-
inst.record = (value: number, attrs?: Attributes) =>
71-
original(value, attrs ? stripTenantAttrs(attrs) : attrs)
72-
}
73-
if (typeof inst.add === 'function') {
74-
const original = inst.add.bind(inst)
75-
inst.add = (value: number, attrs?: Attributes) =>
76-
original(value, attrs ? stripTenantAttrs(attrs) : attrs)
77-
}
78-
79-
return instrument
53+
return factory()
8054
}
8155

8256
// ============================================================================

src/internal/monitoring/otel-metrics.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,6 @@ const gcHistogramAggregation = {
110110
const dropAggregation = { type: AggregationType.DROP } as const
111111

112112
// Views — custom histogram buckets + drop auto-instrumentation duplicates.
113-
// Tenant attribute stripping is handled by registerMetric() in metrics.ts.
114113
const views = [
115114
{
116115
meterName: 'storage-api',

src/storage/database/knex.test.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import { TenantConnection } from '../../internal/database/connection'
2+
import { dbQueryPerformance } from '../../internal/monitoring/metrics'
23
import { escapeLike, StorageKnexDB } from './knex'
34

5+
class TestStorageKnexDB extends StorageKnexDB {
6+
runTestQuery() {
7+
return this.runQuery('TestQuery', async () => 'allowed')
8+
}
9+
}
10+
411
describe('escapeLike', () => {
512
test('escapes SQL wildcard characters', () => {
613
expect(escapeLike('%_abc')).toBe('\\%\\_abc')
@@ -23,15 +30,15 @@ function createStorageKnexTestHarness() {
2330
getAbortSignal: vi.fn().mockReturnValue(undefined),
2431
} as unknown as TenantConnection
2532

26-
const db = new StorageKnexDB(connection, {
33+
const db = new TestStorageKnexDB(connection, {
2734
tenantId: 'test-tenant',
2835
host: 'localhost',
2936
})
3037

3138
return { db, connection, transaction }
3239
}
3340

34-
describe('StorageKnexDB.testPermission', () => {
41+
describe('StorageKnexDB', () => {
3542
it('returns the callback result after rolling back the transaction', async () => {
3643
const { db, connection, transaction } = createStorageKnexTestHarness()
3744

@@ -61,4 +68,19 @@ describe('StorageKnexDB.testPermission', () => {
6168
expect(transaction.rollback).toHaveBeenCalledTimes(1)
6269
expect(transaction.commit).not.toHaveBeenCalled()
6370
})
71+
72+
it('records query performance attributes without tenant id labels', async () => {
73+
const { db } = createStorageKnexTestHarness()
74+
const recordSpy = vi.spyOn(dbQueryPerformance, 'record')
75+
76+
try {
77+
await db.runTestQuery()
78+
79+
expect(recordSpy).toHaveBeenCalledWith(expect.any(Number), {
80+
name: 'TestQuery',
81+
})
82+
} finally {
83+
recordSpy.mockRestore()
84+
}
85+
})
6486
})

src/storage/database/knex.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1084,7 +1084,6 @@ export class StorageKnexDB implements Database {
10841084
const duration = Number(process.hrtime.bigint() - startTime) / 1e9
10851085
dbQueryPerformance.record(duration, {
10861086
name: queryName,
1087-
tenantId: this.tenantId,
10881087
})
10891088
}
10901089

src/storage/uploader.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,6 @@ export class Uploader {
106106
await this.canUpload(options)
107107
fileUploadStarted.add(1, {
108108
uploadType: options.uploadType,
109-
tenantId: this.db.tenantId,
110109
})
111110

112111
return randomUUID()
@@ -284,7 +283,6 @@ export class Uploader {
284283

285284
fileUploadedSuccess.add(1, {
286285
uploadType,
287-
tenantId: this.db.tenantId,
288286
})
289287

290288
return { obj: newObject, isNew, metadata: objectMetadata }

src/test/uploader.test.ts

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { once } from 'events'
22
import { FastifyRequest } from 'fastify'
33
import { PassThrough, Readable } from 'stream'
44
import { ErrorCode, isStorageError, StorageBackendError } from '../internal/errors'
5-
import { ObjectAdminDelete } from '../storage/events'
5+
import { fileUploadedSuccess, fileUploadStarted } from '../internal/monitoring/metrics'
6+
import { ObjectAdminDelete, ObjectCreatedPostEvent } from '../storage/events'
67
import { TenantLocation } from '../storage/locator'
78
import { fileUploadFromRequest, Uploader } from '../storage/uploader'
89

@@ -21,6 +22,21 @@ function createUploader(
2122
new TenantLocation('test-bucket')
2223
)
2324
}
25+
26+
function createUploaderDb(overrides: Partial<UploaderDatabase> = {}) {
27+
const db = {
28+
tenantId: 'stub-tenant',
29+
reqId: 'req-1',
30+
sbReqId: 'sb-req-1',
31+
tenant: () => ({ ref: 'stub-tenant', host: 'stub-tenant.local' }),
32+
testPermission: vi.fn(async () => undefined),
33+
...overrides,
34+
} as Partial<UploaderDatabase> &
35+
Pick<UploaderDatabase, 'tenantId' | 'reqId' | 'tenant' | 'testPermission'>
36+
37+
return db
38+
}
39+
2440
describe('fileUploadFromRequest', () => {
2541
test('keeps multipart/form-data file size undefined even when the request content-length exceeds 5GB', async () => {
2642
const file = Readable.from(['payload']) as Readable & { truncated: boolean }
@@ -380,3 +396,83 @@ describe('fileUploadFromRequest', () => {
380396
completeUploadSpy.mockRestore()
381397
})
382398
})
399+
400+
describe('Uploader metrics', () => {
401+
test('prepareUpload records upload start attributes without tenant id labels', async () => {
402+
const addSpy = vi.spyOn(fileUploadStarted, 'add')
403+
const uploader = createUploader(
404+
{
405+
uploadObject: vi.fn(),
406+
},
407+
createUploaderDb()
408+
)
409+
410+
try {
411+
await uploader.prepareUpload({
412+
bucketId: 'bucket',
413+
objectName: 'test.txt',
414+
owner: undefined,
415+
isUpsert: false,
416+
userMetadata: undefined,
417+
metadata: undefined,
418+
uploadType: 'standard',
419+
})
420+
421+
expect(addSpy).toHaveBeenCalledWith(1, { uploadType: 'standard' })
422+
} finally {
423+
addSpy.mockRestore()
424+
}
425+
})
426+
427+
test('completeUpload records upload success attributes without tenant id labels', async () => {
428+
const addSpy = vi.spyOn(fileUploadedSuccess, 'add')
429+
const sendWebhookSpy = vi
430+
.spyOn(ObjectCreatedPostEvent, 'sendWebhook')
431+
.mockResolvedValue(undefined)
432+
const transactionDb = {
433+
waitObjectLock: vi.fn().mockResolvedValue(undefined),
434+
findObject: vi.fn().mockResolvedValue(undefined),
435+
upsertObject: vi.fn().mockResolvedValue({ id: 'object-id' }),
436+
}
437+
const db = createUploaderDb({
438+
asSuperUser: vi.fn().mockReturnValue({
439+
connection: {
440+
setAbortSignal: vi.fn(),
441+
},
442+
withTransaction: vi.fn(async (fn) => fn(transactionDb)),
443+
}),
444+
})
445+
const uploader = createUploader(
446+
{
447+
uploadObject: vi.fn(),
448+
},
449+
db
450+
)
451+
452+
try {
453+
await uploader.completeUpload({
454+
version: 'version-1',
455+
bucketId: 'bucket',
456+
objectName: 'test.txt',
457+
owner: undefined,
458+
objectMetadata: {
459+
eTag: '"etag"',
460+
mimetype: 'text/plain',
461+
cacheControl: 'max-age=3600',
462+
lastModified: new Date(),
463+
contentLength: 7,
464+
httpStatusCode: 200,
465+
size: 7,
466+
},
467+
uploadType: 'standard',
468+
isUpsert: false,
469+
userMetadata: undefined,
470+
})
471+
472+
expect(addSpy).toHaveBeenCalledWith(1, { uploadType: 'standard' })
473+
} finally {
474+
addSpy.mockRestore()
475+
sendWebhookSpy.mockRestore()
476+
}
477+
})
478+
})

0 commit comments

Comments
 (0)