From 452433a9ae2a9551e852ba7f00d13c61ac2301a9 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Sat, 23 May 2026 09:19:26 +0200 Subject: [PATCH] feat(upgrade): emit twenty_upgrade_instance_info gauge with version attribute (#20854) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `twenty_upgrade_instance_info` — a new "info"-style gauge that carries the inferred instance version (derived from the last applied upgrade migration) as the `version` label. This follows the standard Prometheus pattern for surfacing string-valued metadata: value is always `1` (load-bearing for PromQL `group_left` joins), the data lives on the label. Same shape as `node_uname_info`, `go_info`, `kube_pod_info`, etc. — see [Prometheus naming conventions](https://prometheus.io/docs/practices/naming/#metric-names). On `/metrics`: ``` # HELP twenty_upgrade_instance_info Inferred instance version (semver-ish, derived from the last applied upgrade migration), carried as the `version` attribute # TYPE twenty_upgrade_instance_info gauge twenty_upgrade_instance_info{version="2.7.3"} 1 ``` Also adds `MetricsService.createInfoGauge` as the helper for the pattern (auto-suffixes `_info`, enforces value=1). Consumed by twentyhq/twenty-eng#65. --- .../core-modules/metrics/metrics.service.ts | 34 +++++++++++++++++++ .../upgrade/upgrade-gauge.service.ts | 16 +++++++++ 2 files changed, 50 insertions(+) diff --git a/packages/twenty-server/src/engine/core-modules/metrics/metrics.service.ts b/packages/twenty-server/src/engine/core-modules/metrics/metrics.service.ts index a77062d008..668ad58402 100644 --- a/packages/twenty-server/src/engine/core-modules/metrics/metrics.service.ts +++ b/packages/twenty-server/src/engine/core-modules/metrics/metrics.service.ts @@ -80,6 +80,40 @@ export class MetricsService { return gauge; } + createInfoGauge({ + metricName, + options, + attributesCallback, + }: { + metricName: string; + options: MetricOptions; + attributesCallback: () => Attributes | Promise; + }): ObservableGauge { + const normalizedName = metricName.endsWith('_info') + ? metricName + : `${metricName}_info`; + + const gauge = this.getMeter().createObservableGauge( + normalizedName, + options, + ); + + gauge.addCallback(async (observableResult) => { + try { + const attributes = await attributesCallback(); + + observableResult.observe(1, attributes); + } catch (error) { + this.logger.error( + `Failed to collect info gauge ${normalizedName}`, + error, + ); + } + }); + + return gauge; + } + async incrementCounterForEvent({ key, eventId, diff --git a/packages/twenty-server/src/engine/core-modules/upgrade/upgrade-gauge.service.ts b/packages/twenty-server/src/engine/core-modules/upgrade/upgrade-gauge.service.ts index 3afae6bf49..b64a0080cf 100644 --- a/packages/twenty-server/src/engine/core-modules/upgrade/upgrade-gauge.service.ts +++ b/packages/twenty-server/src/engine/core-modules/upgrade/upgrade-gauge.service.ts @@ -92,6 +92,22 @@ export class UpgradeGaugeService implements OnModuleInit { }, cacheValue: true, }); + + this.metricsService.createInfoGauge({ + metricName: 'twenty_upgrade_instance', + options: { + description: + 'Inferred instance version (semver-ish, derived from the last applied upgrade migration), carried as the `version` attribute', + }, + attributesCallback: async () => { + const upgradeStatus = await this.getCachedUpgradeStatus(); + + return { + version: + upgradeStatus?.instanceUpgradeStatus.inferredVersion ?? 'unknown', + }; + }, + }); } private async getCachedUpgradeStatus(): Promise {