UNPKG

@temporalio/common

Version:

Common library for code that's used across the Client, Worker, and/or Workflow

486 lines (413 loc) 14.4 kB
import { filterNullAndUndefined, mergeObjects } from './internal-workflow'; /** * A meter for creating metrics to record values on. * * @experimental The Metric API is an experimental feature and may be subject to change. */ export interface MetricMeter { /** * Create a new counter metric that supports adding values. * * @param name Name for the counter metric. * @param unit Unit for the counter metric. Optional. * @param description Description for the counter metric. Optional. */ createCounter(name: string, unit?: string, description?: string): MetricCounter; /** * Create a new histogram metric that supports recording values. * * @param name Name for the histogram metric. * @param valueType Type of value to record. Defaults to `int`. * @param unit Unit for the histogram metric. Optional. * @param description Description for the histogram metric. Optional. */ createHistogram( name: string, valueType?: NumericMetricValueType, unit?: string, description?: string ): MetricHistogram; /** * Create a new gauge metric that supports setting values. * * @param name Name for the gauge metric. * @param valueType Type of value to set. Defaults to `int`. * @param unit Unit for the gauge metric. Optional. * @param description Description for the gauge metric. Optional. */ createGauge(name: string, valueType?: NumericMetricValueType, unit?: string, description?: string): MetricGauge; /** * Return a clone of this meter, with additional tags. All metrics created off the meter will * have the tags. * * @param tags Tags to append. */ withTags(tags: MetricTags): MetricMeter; } /** * Base interface for all metrics. * * @experimental The Metric API is an experimental feature and may be subject to change. */ export interface Metric { /** * The name of the metric. */ name: string; /** * The unit of the metric, if any. */ unit?: string; /** * The description of the metric, if any. */ description?: string; /** * The kind of the metric (e.g. `counter`, `histogram`, `gauge`). */ kind: MetricKind; /** * The type of value recorded by the metric. Either `int` or `float`. */ valueType: NumericMetricValueType; } /** * Tags to be attached to some metrics. * * @experimental The Metric API is an experimental feature and may be subject to change. */ export type MetricTags = Record<string, string | number | boolean>; /** * Type of numerical values recorded by a metric. * * Note that this represents the _configuration_ of the metric; however, since JavaScript doesn't * have different runtime representation for integers and floats, the actual value type is always * a JS 'number'. * * @experimental The Metric API is an experimental feature and may be subject to change. */ export type NumericMetricValueType = 'int' | 'float'; /** * The kind of a metric. * * @experimental The Metric API is an experimental feature and may be subject to change. */ export type MetricKind = 'counter' | 'histogram' | 'gauge'; /** * A metric that supports adding values as a counter. * * @experimental The Metric API is an experimental feature and may be subject to change. */ export interface MetricCounter extends Metric { /** * Add the given value to the counter. * * @param value Value to add. * @param extraTags Extra tags if any. */ add(value: number, extraTags?: MetricTags): void; /** * Return a clone of this counter, with additional tags. * * @param tags Tags to append to existing tags. */ withTags(tags: MetricTags): MetricCounter; kind: 'counter'; valueType: 'int'; } /** * A metric that supports recording values on a histogram. * * @experimental The Metric API is an experimental feature and may be subject to change. */ export interface MetricHistogram extends Metric { /** * Record the given value on the histogram. * * @param value Value to record. Must be a non-negative number. Value will be casted to the given * {@link valueType}. Loss of precision may occur if the value is not already of the * correct type. * @param extraTags Extra tags if any. */ record(value: number, extraTags?: MetricTags): void; /** * Return a clone of this histogram, with additional tags. * * @param tags Tags to append to existing tags. */ withTags(tags: MetricTags): MetricHistogram; kind: 'histogram'; } /** * A metric that supports setting values. * * @experimental The Metric API is an experimental feature and may be subject to change. */ export interface MetricGauge extends Metric { /** * Set the given value on the gauge. * * @param value Value to set. * @param extraTags Extra tags if any. */ set(value: number, extraTags?: MetricTags): void; /** * Return a clone of this gauge, with additional tags. * * @param tags Tags to append to existing tags. */ withTags(tags: MetricTags): MetricGauge; kind: 'gauge'; } //////////////////////////////////////////////////////////////////////////////////////////////////// /** * A meter implementation that does nothing. */ class NoopMetricMeter implements MetricMeter { createCounter(name: string, unit?: string, description?: string): MetricCounter { return { name, unit, description, kind: 'counter', valueType: 'int', add(_value, _extraTags) {}, withTags(_extraTags) { return this; }, }; } createHistogram( name: string, valueType: NumericMetricValueType = 'int', unit?: string, description?: string ): MetricHistogram { return { name, unit, description, kind: 'histogram', valueType, record(_value, _extraTags) {}, withTags(_extraTags) { return this; }, }; } createGauge( name: string, valueType: NumericMetricValueType = 'int', unit?: string, description?: string ): MetricGauge { return { name, unit, description, kind: 'gauge', valueType, set(_value, _extraTags) {}, withTags(_extraTags) { return this; }, }; } withTags(_extraTags: MetricTags): MetricMeter { return this; } } export const noopMetricMeter = new NoopMetricMeter(); //////////////////////////////////////////////////////////////////////////////////////////////////// export type MetricTagsOrFunc = MetricTags | (() => MetricTags); /** * A meter implementation that adds tags before delegating calls to a parent meter. * * @experimental The Metric API is an experimental feature and may be subject to change. * @internal * @hidden */ export class MetricMeterWithComposedTags implements MetricMeter { /** * Return a {@link MetricMeter} that adds tags before delegating calls to a parent meter. * * New tags may either be specified statically as a delta object, or as a function evaluated * every time a metric is recorded that will return a delta object. * * Some optimizations are performed to avoid creating unnecessary objects and to keep runtime * overhead associated with resolving tags as low as possible. * * @param meter The parent meter to delegate calls to. * @param tagsOrFunc New tags may either be specified statically as a delta object, or as a function * evaluated every time a metric is recorded that will return a delta object. * @param force if `true`, then a `MetricMeterWithComposedTags` will be created even if there * is no tags to add. This is useful to add tags support to an underlying meter * implementation that does not support tags directly. */ public static compose(meter: MetricMeter, tagsOrFunc: MetricTagsOrFunc, force: boolean = false): MetricMeter { if (meter instanceof MetricMeterWithComposedTags) { const contributors = appendToChain(meter.contributors, tagsOrFunc); // If the new contributor results in no actual change to the chain, then we don't need a new meter if (contributors === undefined && !force) return meter; return new MetricMeterWithComposedTags(meter.parentMeter, contributors ?? []); } else { const contributors = appendToChain(undefined, tagsOrFunc); if (contributors === undefined && !force) return meter; return new MetricMeterWithComposedTags(meter, contributors ?? []); } } private constructor( private readonly parentMeter: MetricMeter, private readonly contributors: MetricTagsOrFunc[] ) {} createCounter(name: string, unit?: string, description?: string): MetricCounter { const parentCounter = this.parentMeter.createCounter(name, unit, description); return new MetricCounterWithComposedTags(parentCounter, this.contributors); } createHistogram( name: string, valueType: NumericMetricValueType = 'int', unit?: string, description?: string ): MetricHistogram { const parentHistogram = this.parentMeter.createHistogram(name, valueType, unit, description); return new MetricHistogramWithComposedTags(parentHistogram, this.contributors); } createGauge( name: string, valueType: NumericMetricValueType = 'int', unit?: string, description?: string ): MetricGauge { const parentGauge = this.parentMeter.createGauge(name, valueType, unit, description); return new MetricGaugeWithComposedTags(parentGauge, this.contributors); } withTags(tags: MetricTags): MetricMeter { return MetricMeterWithComposedTags.compose(this, tags); } } /** * @experimental The Metric API is an experimental feature and may be subject to change. */ class MetricCounterWithComposedTags implements MetricCounter { public readonly kind = 'counter'; public readonly valueType = 'int'; constructor( private parentCounter: MetricCounter, private contributors: MetricTagsOrFunc[] ) {} add(value: number, extraTags?: MetricTags | undefined): void { this.parentCounter.add(value, resolveTags(this.contributors, extraTags)); } withTags(extraTags: MetricTags): MetricCounter { const contributors = appendToChain(this.contributors, extraTags); if (contributors === undefined) return this; return new MetricCounterWithComposedTags(this.parentCounter, contributors); } get name(): string { return this.parentCounter.name; } get unit(): string | undefined { return this.parentCounter.unit; } get description(): string | undefined { return this.parentCounter.description; } } /** * @experimental The Metric API is an experimental feature and may be subject to change. */ class MetricHistogramWithComposedTags implements MetricHistogram { public readonly kind = 'histogram'; constructor( private parentHistogram: MetricHistogram, private contributors: MetricTagsOrFunc[] ) {} record(value: number, extraTags?: MetricTags): void { this.parentHistogram.record(value, resolveTags(this.contributors, extraTags)); } withTags(extraTags: MetricTags): MetricHistogram { const contributors = appendToChain(this.contributors, extraTags); if (contributors === undefined) return this; return new MetricHistogramWithComposedTags(this.parentHistogram, contributors); } get name(): string { return this.parentHistogram.name; } get valueType(): NumericMetricValueType { return this.parentHistogram.valueType; } get unit(): string | undefined { return this.parentHistogram.unit; } get description(): string | undefined { return this.parentHistogram.description; } } /** * @internal * @hidden */ class MetricGaugeWithComposedTags implements MetricGauge { public readonly kind = 'gauge'; constructor( private parentGauge: MetricGauge, private contributors: MetricTagsOrFunc[] ) {} set(value: number, extraTags?: MetricTags): void { this.parentGauge.set(value, resolveTags(this.contributors, extraTags)); } withTags(extraTags: MetricTags): MetricGauge { const contributors = appendToChain(this.contributors, extraTags); if (contributors === undefined) return this; return new MetricGaugeWithComposedTags(this.parentGauge, contributors); } get name(): string { return this.parentGauge.name; } get valueType(): NumericMetricValueType { return this.parentGauge.valueType; } get unit(): string | undefined { return this.parentGauge.unit; } get description(): string | undefined { return this.parentGauge.description; } } function resolveTags(contributors: MetricTagsOrFunc[], extraTags?: MetricTags): MetricTags { const resolved = {}; for (const contributor of contributors) { Object.assign(resolved, typeof contributor === 'function' ? contributor() : contributor); } Object.assign(resolved, extraTags); return filterNullAndUndefined(resolved); } /** * Append a tags contributor to the chain, merging it with the former last contributor if possible. * * If appending the new contributor results in no actual change to the chain of contributors, return * `existingContributors`; in that case, the caller should avoid creating a new object if possible. */ function appendToChain( existingContributors: MetricTagsOrFunc[] | undefined, newContributor: MetricTagsOrFunc ): MetricTagsOrFunc[] | undefined { // If the new contributor is an empty object, then it results in no actual change to the chain if (typeof newContributor === 'object' && Object.keys(newContributor).length === 0) { return existingContributors; } // If existing chain is empty, then the new contributor is the chain if (existingContributors == null || existingContributors.length === 0) { return [newContributor]; } // If both last contributor and new contributor are plain objects, merge them to a single object. const last = existingContributors[existingContributors.length - 1]; if (typeof last === 'object' && typeof newContributor === 'object') { const merged = mergeObjects(last, newContributor); if (merged === last) return existingContributors; return [...existingContributors.slice(0, -1), merged!]; } // Otherwise, just append the new contributor to the chain. return [...existingContributors, newContributor]; }