homebridge-prometheus-exporter/src/prometheus.ts

71 lines
2.4 KiB
TypeScript
Raw Normal View History

2022-11-07 20:49:11 +01:00
import { Metric } from './metrics'
function escapeString(str: string) {
return str.replace(/\\/g, '\\\\').replace(/\n/g, '\\n')
}
/**
* String Attribute values are converted directly to Prometheus attribute values.
* Non-string values are represented as JSON-encoded strings.
*
* `undefined` is converted to an empty string.
*/
function escapeAttributeValue(str: string) {
if (typeof str !== 'string') {
str = JSON.stringify(str)
}
return escapeString(str).replace(/"/g, '\\"')
}
const invalidCharacterRegex = /[^a-z0-9_]/gi
/**
* Ensures metric names are valid Prometheus metric names by removing
* characters allowed by OpenTelemetry but disallowed by Prometheus.
*
* https://prometheus.io/docs/concepts/data_model/#metric-names-and-attributes
*
* 1. Names must match `[a-zA-Z_:][a-zA-Z0-9_:]*`
*
* 2. Colons are reserved for user defined recording rules.
* They should not be used by exporters or direct instrumentation.
*
* OpenTelemetry metric names are already validated in the Meter when they are created,
* and they match the format `[a-zA-Z][a-zA-Z0-9_.\-]*` which is very close to a valid
* prometheus metric name, so we only need to strip characters valid in OpenTelemetry
* but not valid in prometheus and replace them with '_'.
*
* @param name name to be sanitized
*/
function sanitizePrometheusMetricName(name: string): string {
return name.replace(invalidCharacterRegex, '_') // replace all invalid characters with '_'
}
export class MetricsRenderer {
constructor(private readonly prefix: string) {}
render(metric: Metric): string {
const name = this.metricName(metric.name)
return [
`# TYPE ${name} ${name.endsWith('_total') ? 'count' : 'gauge'}`,
`${name}${this.renderLabels(metric.labels)} ${metric.value}${
metric.timestamp !== null ? ' ' + String(metric.timestamp.getTime()) : ''
}`,
].join('\n')
}
private renderLabels(labels: Metric['labels']): string {
const rendered = Object.entries(labels)
.map(([key, value]) => `${sanitizePrometheusMetricName(key)}="${escapeAttributeValue(value)}"`)
.join(',')
return rendered !== '' ? '{' + rendered + '}' : ''
}
private metricName(name: string): string {
name = name.replace(/^(.*_)?(total)_(.*)$/, '$1$3_$2')
return sanitizePrometheusMetricName(this.prefix.replace(/_+$/, '') + '_' + name)
}
}