Add prometheus exporter (#17392)

Create a metric endpoint and expose prometheus gauge for event stream
count.
This commit is contained in:
Thomas Trompette
2026-01-23 16:38:15 +01:00
committed by GitHub
parent 4c94e650a7
commit 2346efd71f
8 changed files with 124 additions and 9 deletions
@@ -206,6 +206,38 @@ export class CacheStorageService {
} while (cursor !== 0);
}
async scanAndCountSetMembers(scanPattern: string): Promise<number> {
if (!this.isRedisCache()) {
throw new Error(
'scanAndCountSetMembers is only supported with Redis cache',
);
}
const redisClient = (this.cache as RedisCache).store.client;
let cursor = 0;
let totalCount = 0;
do {
const result = await redisClient.scan(cursor, {
MATCH: `${this.namespace}:${scanPattern}`,
COUNT: 100,
});
cursor = result.cursor;
const keys = result.keys;
if (keys.length > 0) {
const counts = await Promise.all(
keys.map((key) => redisClient.sCard(key)),
);
totalCount += counts.reduce((sum, count) => sum + count, 0);
}
} while (cursor !== 0);
return totalCount;
}
async acquireLock(key: string, ttl = 1000): Promise<boolean> {
if (!this.isRedisCache()) {
throw new Error('acquireLock is only supported with Redis cache');
@@ -1,14 +1,39 @@
import { Injectable } from '@nestjs/common';
import { metrics, type Attributes } from '@opentelemetry/api';
import {
metrics,
type Attributes,
type Meter,
type MetricOptions,
type ObservableGauge,
type ObservableResult,
} from '@opentelemetry/api';
import { MetricsCacheService } from 'src/engine/core-modules/metrics/metrics-cache.service';
import { type MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
const METER_NAME = 'twenty-server';
@Injectable()
export class MetricsService {
constructor(private readonly metricsCacheService: MetricsCacheService) {}
getMeter(): Meter {
return metrics.getMeter(METER_NAME);
}
createObservableGauge(
name: string,
options: MetricOptions,
callback: (observableResult: ObservableResult) => void | Promise<void>,
): ObservableGauge {
const gauge = this.getMeter().createObservableGauge(name, options);
gauge.addCallback(callback);
return gauge;
}
async incrementCounter({
key,
eventId,
@@ -20,9 +45,7 @@ export class MetricsService {
attributes?: Attributes;
shouldStoreInCache?: boolean;
}) {
//TODO : Define meter name usage in monitoring
const meter = metrics.getMeter('twenty-server');
const counter = meter.createCounter(key);
const counter = this.getMeter().createCounter(key);
counter.add(1, attributes);
@@ -42,9 +65,7 @@ export class MetricsService {
attributes?: Attributes;
shouldStoreInCache?: boolean;
}) {
//TODO : Define meter name usage in monitoring
const meter = metrics.getMeter('twenty-server');
const counter = meter.createCounter(key);
const counter = this.getMeter().createCounter(key);
counter.add(eventIds.length, attributes);
@@ -1,4 +1,5 @@
export enum MeterDriver {
OpenTelemetry = 'opentelemetry',
Console = 'console',
Prometheus = 'prometheus',
}