Files
Compass/backend/shared/src/monitoring/metrics.ts
2025-08-27 21:30:05 +02:00

173 lines
4.4 KiB
TypeScript

import { isEqual, flatten } from 'lodash'
// see https://cloud.google.com/monitoring/api/ref_v3/rest/v3/projects.metricDescriptors#MetricKind
export type MetricKind = 'GAUGE' | 'CUMULATIVE'
// see https://cloud.google.com/monitoring/api/ref_v3/rest/v3/TypedValue
export type MetricValueKind =
| 'int64Value'
| 'doubleValue'
| 'stringValue'
| 'boolValue'
| 'distributionValue'
export type MetricDescriptor = {
metricKind: MetricKind
valueKind: MetricValueKind
}
export type MetricLabels = Record<string, string>
export const CUSTOM_METRICS = {
'ws/open_connections': {
metricKind: 'GAUGE',
valueKind: 'int64Value',
},
'ws/connections_established': {
metricKind: 'CUMULATIVE',
valueKind: 'int64Value',
},
'ws/connections_terminated': {
metricKind: 'CUMULATIVE',
valueKind: 'int64Value',
},
'ws/broadcasts_sent': {
metricKind: 'CUMULATIVE',
valueKind: 'int64Value',
},
'http/request_count': {
metricKind: 'CUMULATIVE',
valueKind: 'int64Value',
},
'http/request_latency': {
metricKind: 'GAUGE',
valueKind: 'distributionValue',
},
'app/bet_count': {
metricKind: 'CUMULATIVE',
valueKind: 'int64Value',
},
'app/contract_view_count': {
metricKind: 'CUMULATIVE',
valueKind: 'int64Value',
},
'pg/query_count': {
metricKind: 'CUMULATIVE',
valueKind: 'int64Value',
},
'pg/connections_established': {
metricKind: 'CUMULATIVE',
valueKind: 'int64Value',
},
'pg/connections_terminated': {
metricKind: 'CUMULATIVE',
valueKind: 'int64Value',
},
'pg/connections_acquired': {
metricKind: 'CUMULATIVE',
valueKind: 'int64Value',
},
'pg/connections_released': {
metricKind: 'CUMULATIVE',
valueKind: 'int64Value',
},
'pg/pool_connections': {
metricKind: 'GAUGE',
valueKind: 'int64Value',
},
'vercel/revalidations_succeeded': {
metricKind: 'CUMULATIVE',
valueKind: 'int64Value',
},
'vercel/revalidations_failed': {
metricKind: 'CUMULATIVE',
valueKind: 'int64Value',
},
} as const satisfies { [k: string]: MetricDescriptor }
// the typing for all this could be way fancier, but seems overkill
export type CustomMetrics = typeof CUSTOM_METRICS
export type MetricType = keyof CustomMetrics
export type MetricStoreEntry = {
type: MetricType
labels?: MetricLabels
fresh: boolean // whether this metric was touched since last time
startTime: number // used for cumulative metrics
points?: number[] // used for distribution metrics
value: number
}
/** Records metric values by type and labels for later export. */
export class MetricStore {
// { name: [...entries of that metric with different label values] }
data: Map<MetricType, MetricStoreEntry[]>
constructor() {
this.data = new Map()
}
clear() {
this.data.clear()
}
push(type: MetricType, val: number, labels?: MetricLabels) {
const entry = this.getOrCreate(type, labels)
let points = entry.points
if (points == null) {
points = entry.points = []
}
points.push(val)
entry.fresh = true
}
set(type: MetricType, val: number, labels?: MetricLabels) {
const entry = this.getOrCreate(type, labels)
entry.value = val
entry.fresh = true
}
inc(type: MetricType, labels?: MetricLabels) {
const entry = this.getOrCreate(type, labels)
entry.value += 1
entry.fresh = true
}
freshEntries() {
return flatten(
Array.from(this.data.entries(), ([_, vs]) => vs.filter((e) => e.fresh))
)
}
// mqp: we could clear all gauges but then we should centralize the process for polling
// them in order to not have weird gaps.
clearDistributionGauges() {
for (const k of this.data.keys()) {
const { metricKind, valueKind } = CUSTOM_METRICS[k]
if (metricKind === 'GAUGE' && valueKind === 'distributionValue') {
this.data.delete(k)
}
}
}
getOrCreate(type: MetricType, labels?: MetricLabels) {
let entries = this.data.get(type)
if (entries == null) {
this.data.set(type, (entries = []))
}
for (const entry of entries) {
if (isEqual(labels ?? {}, entry.labels ?? {})) {
return entry
}
}
// none exists, so create it
const entry = { type, labels, startTime: Date.now(), fresh: true, value: 0 }
entries.push(entry)
return entry as MetricStoreEntry
}
}
/** The global metric store. */
export const metrics = new MetricStore()