@temporalio/common
Version:
Common library for code that's used across the Client, Worker, and/or Workflow
486 lines (413 loc) • 14.4 kB
text/typescript
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];
}