Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: type safe metric labels #479

Merged
merged 1 commit into from
Jan 8, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 20 additions & 34 deletions src/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,58 +19,45 @@ export enum MessageSource {
publish = 'publish'
}

type LabelsGeneric = Record<string, string | undefined>
type NoLabels = Record<string, never>
type LabelsGeneric = Record<string, string | number>
type LabelKeys<Labels extends LabelsGeneric> = Extract<keyof Labels, string>
interface CollectFn<Labels extends LabelsGeneric> { (metric: Gauge<Labels>): void }

export interface Gauge<Labels extends LabelsGeneric = never> {
// Sorry for this mess, `prom-client` API choices are not great
// If the function signature was `inc(value: number, labels?: Labels)`, this would be simpler
inc(value?: number): void
inc(labels: Labels, value?: number): void
inc(arg1?: Labels | number, arg2?: number): void

set(value: number): void
set(labels: Labels, value: number): void
set(arg1?: Labels | number, arg2?: number): void
export interface Gauge<Labels extends LabelsGeneric = NoLabels> {
inc: NoLabels extends Labels ? (value?: number) => void : (labels: Labels, value?: number) => void
set: NoLabels extends Labels ? (value: number) => void : (labels: Labels, value: number) => void

addCollect(collectFn: CollectFn<Labels>): void
}

export interface Histogram<Labels extends LabelsGeneric = never> {
export interface Histogram<Labels extends LabelsGeneric = NoLabels> {
startTimer(): () => void

observe(value: number): void
observe(labels: Labels, values: number): void
observe(arg1: Labels | number, arg2?: number): void
observe: NoLabels extends Labels ? (value: number) => void : (labels: Labels, value: number) => void

reset(): void
}

export interface AvgMinMax<Labels extends LabelsGeneric = never> {
set(values: number[]): void
set(labels: Labels, values: number[]): void
set(arg1?: Labels | number[], arg2?: number[]): void
export interface AvgMinMax<Labels extends LabelsGeneric = NoLabels> {
set: NoLabels extends Labels ? (values: number[]) => void : (labels: Labels, values: number[]) => void
}

export interface GaugeConfig<Labels extends LabelsGeneric> {
export type GaugeConfig<Labels extends LabelsGeneric> = {
name: string
help: string
labelNames?: keyof Labels extends string ? Array<keyof Labels> : undefined
}
} & (NoLabels extends Labels ? { labelNames?: never } : { labelNames: [LabelKeys<Labels>, ...Array<LabelKeys<Labels>>] })

export interface HistogramConfig<Labels extends LabelsGeneric> {
name: string
help: string
labelNames?: Array<keyof Labels>
export type HistogramConfig<Labels extends LabelsGeneric> = GaugeConfig<Labels> & {
buckets?: number[]
}

export type AvgMinMaxConfig<Labels extends LabelsGeneric> = GaugeConfig<Labels>

export interface MetricsRegister {
gauge<T extends LabelsGeneric>(config: GaugeConfig<T>): Gauge<T>
histogram<T extends LabelsGeneric>(config: HistogramConfig<T>): Histogram<T>
avgMinMax<T extends LabelsGeneric>(config: AvgMinMaxConfig<T>): AvgMinMax<T>
gauge<Labels extends LabelsGeneric = NoLabels>(config: GaugeConfig<Labels>): Gauge<Labels>
histogram<Labels extends LabelsGeneric = NoLabels>(config: HistogramConfig<Labels>): Histogram<Labels>
avgMinMax<Labels extends LabelsGeneric = NoLabels>(config: AvgMinMaxConfig<Labels>): AvgMinMax<Labels>
}

export enum InclusionReason {
Expand Down Expand Up @@ -328,10 +315,9 @@ export function getMetrics (
labelNames: ['hit']
}),

asyncValidationDelayFromFirstSeenSec: register.histogram<{ topic: TopicLabel }>({
asyncValidationDelayFromFirstSeenSec: register.histogram({
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Label value is not set for this metric

this.asyncValidationDelayFromFirstSeenSec.observe((Date.now() - firstSeenTimestampMs) / 1000)

Topic might not be available when setting the metric hence I updated the type instead of updating the code and setting the topic.

name: 'gossipsub_async_validation_delay_from_first_seen',
help: 'Async validation report delay from first seen in second',
labelNames: ['topic'],
buckets: [0.01, 0.03, 0.1, 0.3, 1, 3, 10]
}),

Expand Down Expand Up @@ -482,7 +468,7 @@ export function getMetrics (
labelNames: ['topic']
}),
/** Track duplicate message delivery time */
duplicateMsgDeliveryDelay: register.histogram({
duplicateMsgDeliveryDelay: register.histogram<{ topic: TopicLabel }>({
name: 'gossisub_duplicate_msg_delivery_delay_seconds',
help: 'Time since the 1st duplicated message validated',
labelNames: ['topic'],
Expand Down Expand Up @@ -883,9 +869,9 @@ export function getMetrics (
},

onDuplicateMsgDelivery (topicStr: TopicStr, deliveryDelayMs: number, isLateDelivery: boolean): void {
this.duplicateMsgDeliveryDelay.observe(deliveryDelayMs / 1000)
const topic = this.toTopic(topicStr)
this.duplicateMsgDeliveryDelay.observe({ topic }, deliveryDelayMs / 1000)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Label was defined in type but not set. I'd assume this was just an oversight as topic is always available here.

if (isLateDelivery) {
const topic = this.toTopic(topicStr)
this.duplicateMsgLateDelivery.inc({ topic }, 1)
}
},
Expand Down