feat(upgrade): emit twenty_upgrade_instance_info gauge with version attribute (#20854)

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.
This commit is contained in:
Charles Bochet
2026-05-23 09:19:26 +02:00
committed by GitHub
parent 3bda05ea57
commit 452433a9ae
2 changed files with 50 additions and 0 deletions
@@ -80,6 +80,40 @@ export class MetricsService {
return gauge;
}
createInfoGauge({
metricName,
options,
attributesCallback,
}: {
metricName: string;
options: MetricOptions;
attributesCallback: () => Attributes | Promise<Attributes>;
}): 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,
@@ -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<InstanceAndAllWorkspacesUpgradeStatus | null> {