managarten/packages/shared-storage/src/metrics.ts
Till JS 8c2aa261e8 perf(auth): replace bcrypt with bcryptjs (pure JS, no native build tools)
- Switch from bcrypt (native C++ addon) to bcryptjs (pure JavaScript)
- Remove python3/make/g++ build tools from Dockerfile builder stage
- bcryptjs is 100% hash-compatible with bcrypt
- Smaller builder image and faster Docker builds

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 19:46:16 +01:00

168 lines
4.9 KiB
TypeScript

import type { StorageHooks } from './hooks';
/**
* Minimal interface matching MetricsService.createCounter/createHistogram.
* This avoids a hard dependency on @manacore/shared-nestjs-metrics or prom-client.
*/
export interface MetricsFactory {
createCounter(name: string, help: string, labelNames?: string[]): CounterLike;
createHistogram(
name: string,
help: string,
labelNames?: string[],
buckets?: number[]
): HistogramLike;
}
interface CounterLike {
inc(labels?: Record<string, string | number>, value?: number): void;
}
interface HistogramLike {
observe(labels: Record<string, string | number>, value: number): void;
}
/**
* Interface for a metrics collector — decoupled from prom-client so shared-storage
* stays dependency-free. NestJS backends wire this up with their MetricsService.
*/
export interface StorageMetricsCollector {
incrementUploads(bucket: string, contentType?: string): void;
incrementUploadErrors(bucket: string): void;
incrementDeletes(bucket: string, count: number): void;
incrementDownloads(bucket: string): void;
observeUploadSize(bucket: string, sizeBytes: number): void;
}
/**
* In-memory metrics collector for environments without Prometheus.
* Useful for testing and local development.
*/
export class InMemoryMetrics implements StorageMetricsCollector {
readonly counters = {
uploads: 0,
uploadErrors: 0,
deletes: 0,
downloads: 0,
};
readonly sizes: number[] = [];
incrementUploads(_bucket: string, _contentType?: string): void {
this.counters.uploads++;
}
incrementUploadErrors(_bucket: string): void {
this.counters.uploadErrors++;
}
incrementDeletes(_bucket: string, count: number): void {
this.counters.deletes += count;
}
incrementDownloads(_bucket: string): void {
this.counters.downloads++;
}
observeUploadSize(_bucket: string, sizeBytes: number): void {
this.sizes.push(sizeBytes);
}
reset(): void {
this.counters.uploads = 0;
this.counters.uploadErrors = 0;
this.counters.deletes = 0;
this.counters.downloads = 0;
this.sizes.length = 0;
}
}
/**
* Wires a StorageMetricsCollector to StorageHooks.
* Call this once after creating the hooks + collector to auto-track metrics.
*
* @example
* const hooks = new StorageHooks();
* const collector = createPrometheusCollector(metricsService);
* attachMetrics(hooks, collector);
*/
export function attachMetrics(hooks: StorageHooks, collector: StorageMetricsCollector): () => void {
const unsubs = [
hooks.on('upload', (payload) => {
collector.incrementUploads(payload.bucket, payload.contentType);
if (payload.sizeBytes != null) {
collector.observeUploadSize(payload.bucket, payload.sizeBytes);
}
}),
hooks.on('upload:error', (payload) => {
collector.incrementUploadErrors(payload.bucket);
}),
hooks.on('delete', (payload) => {
collector.incrementDeletes(payload.bucket, payload.keys.length);
}),
hooks.on('download', (payload) => {
collector.incrementDownloads(payload.bucket);
}),
];
return () => unsubs.forEach((unsub) => unsub());
}
/**
* Create a StorageMetricsCollector backed by Prometheus counters/histograms.
* Pass your NestJS MetricsService (or anything that matches MetricsFactory).
*
* @example
* // In a NestJS service
* import { MetricsService } from '@manacore/shared-nestjs-metrics';
* import { createPrometheusCollector, attachMetrics } from '@manacore/shared-storage';
*
* const storage = createPictureStorage();
* const collector = createPrometheusCollector(metricsService);
* attachMetrics(storage.hooks, collector);
*/
export function createPrometheusCollector(factory: MetricsFactory): StorageMetricsCollector {
const uploadsCounter = factory.createCounter(
'storage_uploads_total',
'Total storage upload operations',
['bucket', 'content_type']
);
const uploadErrorsCounter = factory.createCounter(
'storage_upload_errors_total',
'Total storage upload errors',
['bucket']
);
const deletesCounter = factory.createCounter(
'storage_deletes_total',
'Total storage delete operations',
['bucket']
);
const downloadsCounter = factory.createCounter(
'storage_downloads_total',
'Total storage download operations',
['bucket']
);
const uploadSizeHistogram = factory.createHistogram(
'storage_upload_size_bytes',
'Upload file sizes in bytes',
['bucket'],
[1024, 10240, 102400, 1048576, 10485760, 104857600] // 1KB, 10KB, 100KB, 1MB, 10MB, 100MB
);
return {
incrementUploads(bucket: string, contentType?: string): void {
uploadsCounter.inc({ bucket, content_type: contentType ?? 'unknown' });
},
incrementUploadErrors(bucket: string): void {
uploadErrorsCounter.inc({ bucket });
},
incrementDeletes(bucket: string, count: number): void {
deletesCounter.inc({ bucket }, count);
},
incrementDownloads(bucket: string): void {
downloadsCounter.inc({ bucket });
},
observeUploadSize(bucket: string, sizeBytes: number): void {
uploadSizeHistogram.observe({ bucket }, sizeBytes);
},
};
}