Skip to content

Commit 8271248

Browse files
committed
Metrics: Define Metrics and Meter types
1 parent 9aae0d2 commit 8271248

40 files changed

+2092
-170
lines changed

packages/activity/src/index.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
*/
7171

7272
import { AsyncLocalStorage } from 'node:async_hooks';
73-
import { Logger, Duration, LogLevel, LogMetadata, Priority } from '@temporalio/common';
73+
import { Logger, Duration, LogLevel, LogMetadata, MetricMeter, Priority } from '@temporalio/common';
7474
import { msToNumber } from '@temporalio/common/lib/time';
7575
import { SymbolBasedInstanceOfError } from '@temporalio/common/lib/type-helpers';
7676

@@ -281,6 +281,14 @@ export class Context {
281281
*/
282282
public log: Logger;
283283

284+
/**
285+
* Get the metric meter for this activity with activity-specific tags.
286+
*
287+
* To add custom tags, register a {@link ActivityOutboundCallsInterceptor} that
288+
* intercepts the `getMetricTags()` method.
289+
*/
290+
public readonly metricMeter: MetricMeter;
291+
284292
/**
285293
* **Not** meant to instantiated by Activity code, used by the worker.
286294
*
@@ -291,13 +299,15 @@ export class Context {
291299
cancelled: Promise<never>,
292300
cancellationSignal: AbortSignal,
293301
heartbeat: (details?: any) => void,
294-
log: Logger
302+
log: Logger,
303+
metricMeter: MetricMeter
295304
) {
296305
this.info = info;
297306
this.cancelled = cancelled;
298307
this.cancellationSignal = cancellationSignal;
299308
this.heartbeatFn = heartbeat;
300309
this.log = log;
310+
this.metricMeter = metricMeter;
301311
}
302312

303313
/**
@@ -434,3 +444,26 @@ export function cancelled(): Promise<never> {
434444
export function cancellationSignal(): AbortSignal {
435445
return Context.current().cancellationSignal;
436446
}
447+
448+
/**
449+
* Get the metric meter for the current activity, with activity-specific tags.
450+
*
451+
* To add custom tags, register a {@link ActivityOutboundCallsInterceptor} that
452+
* intercepts the `getMetricTags()` method.
453+
*
454+
* This is a shortcut for `Context.current().metricMeter` (see {@link Context.metricMeter}).
455+
*/
456+
export const metricMeter: MetricMeter = {
457+
createCounter(name, unit, description) {
458+
return Context.current().metricMeter.createCounter(name, unit, description);
459+
},
460+
createHistogram(name, valueType = 'int', unit, description) {
461+
return Context.current().metricMeter.createHistogram(name, valueType, unit, description);
462+
},
463+
createGauge(name, valueType = 'int', unit, description) {
464+
return Context.current().metricMeter.createGauge(name, valueType, unit, description);
465+
},
466+
withTags(tags) {
467+
return Context.current().metricMeter.withTags(tags);
468+
},
469+
};

packages/client/src/async-completion-client.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import { status as grpcStatus } from '@grpc/grpc-js';
22
import { ensureTemporalFailure } from '@temporalio/common';
3-
import {
4-
encodeErrorToFailure,
5-
encodeToPayloads,
6-
filterNullAndUndefined,
7-
} from '@temporalio/common/lib/internal-non-workflow';
3+
import { encodeErrorToFailure, encodeToPayloads } from '@temporalio/common/lib/internal-non-workflow';
4+
import { filterNullAndUndefined } from '@temporalio/common/lib/internal-workflow';
85
import { SymbolBasedInstanceOfError } from '@temporalio/common/lib/type-helpers';
96
import {
107
BaseClient,

packages/client/src/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { filterNullAndUndefined } from '@temporalio/common/lib/internal-non-workflow';
1+
import { filterNullAndUndefined } from '@temporalio/common/lib/internal-workflow';
22
import { AsyncCompletionClient } from './async-completion-client';
33
import { BaseClient, BaseClientOptions, defaultBaseClientOptions, LoadedWithDefaults } from './base-client';
44
import { ClientInterceptors } from './interceptors';

packages/client/src/connection.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import { AsyncLocalStorage } from 'node:async_hooks';
22
import * as grpc from '@grpc/grpc-js';
33
import type * as proto from 'protobufjs';
44
import {
5-
filterNullAndUndefined,
65
normalizeTlsConfig,
76
TLSConfig,
87
normalizeGrpcEndpointAddress,
98
} from '@temporalio/common/lib/internal-non-workflow';
9+
import { filterNullAndUndefined } from '@temporalio/common/lib/internal-workflow';
1010
import { Duration, msOptionalToNumber } from '@temporalio/common/lib/time';
1111
import { type temporal } from '@temporalio/proto';
1212
import { isGrpcServiceError, ServiceError } from './errors';

packages/client/src/schedule-client.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,8 @@ import {
77
encodeUnifiedSearchAttributes,
88
} from '@temporalio/common/lib/converter/payload-search-attributes';
99
import { composeInterceptors, Headers } from '@temporalio/common/lib/interceptors';
10-
import {
11-
encodeMapToPayloads,
12-
decodeMapFromPayloads,
13-
filterNullAndUndefined,
14-
} from '@temporalio/common/lib/internal-non-workflow';
10+
import { encodeMapToPayloads, decodeMapFromPayloads } from '@temporalio/common/lib/internal-non-workflow';
11+
import { filterNullAndUndefined } from '@temporalio/common/lib/internal-workflow';
1512
import { temporal } from '@temporalio/proto';
1613
import {
1714
optionalDateToTs,

packages/client/src/task-queue-client.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { status } from '@grpc/grpc-js';
2-
import { filterNullAndUndefined } from '@temporalio/common/lib/internal-non-workflow';
32
import { assertNever, SymbolBasedInstanceOfError, RequireAtLeastOne } from '@temporalio/common/lib/type-helpers';
4-
import { makeProtoEnumConverters } from '@temporalio/common/lib/internal-workflow';
3+
import { filterNullAndUndefined, makeProtoEnumConverters } from '@temporalio/common/lib/internal-workflow';
54
import { temporal } from '@temporalio/proto';
65
import { BaseClient, BaseClientOptions, defaultBaseClientOptions, LoadedWithDefaults } from './base-client';
76
import { WorkflowService } from './types';

packages/client/src/workflow-client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ import {
3434
decodeOptionalFailureToOptionalError,
3535
encodeMapToPayloads,
3636
encodeToPayloads,
37-
filterNullAndUndefined,
3837
} from '@temporalio/common/lib/internal-non-workflow';
38+
import { filterNullAndUndefined } from '@temporalio/common/lib/internal-workflow';
3939
import { temporal } from '@temporalio/proto';
4040
import {
4141
ServiceError,

packages/cloud/src/cloud-operations-client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import { AsyncLocalStorage } from 'node:async_hooks';
22
import * as grpc from '@grpc/grpc-js';
33
import type { RPCImpl } from 'protobufjs';
44
import {
5-
filterNullAndUndefined,
65
normalizeTlsConfig,
76
TLSConfig,
87
normalizeGrpcEndpointAddress,
98
} from '@temporalio/common/lib/internal-non-workflow';
9+
import { filterNullAndUndefined } from '@temporalio/common/lib/internal-workflow';
1010
import { Duration, msOptionalToNumber } from '@temporalio/common/lib/time';
1111
import {
1212
CallContext,

packages/common/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export { Headers, Next } from './interceptors';
2020
export * from './interfaces';
2121
export * from './logger';
2222
export * from './priority';
23+
export * from './metrics';
2324
export * from './retry-policy';
2425
export type { Timestamp, Duration, StringValue } from './time';
2526
export * from './worker-deployments';

packages/common/src/internal-non-workflow/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,3 @@ export * from './data-converter-helpers';
99
export * from './parse-host-uri';
1010
export * from './proxy-config';
1111
export * from './tls-config';
12-
export * from './utils';

packages/common/src/internal-non-workflow/utils.ts

Lines changed: 0 additions & 6 deletions
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './enums-helpers';
2+
export * from './objects-helpers';
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* Helper to prevent `undefined` and `null` values overriding defaults when merging maps.
3+
*/
4+
export function filterNullAndUndefined<T extends Record<string, any>>(obj: T): T {
5+
return Object.fromEntries(Object.entries(obj).filter(([_k, v]) => v != null)) as any;
6+
}
7+
8+
/**
9+
* Merge two objects, possibly removing keys.
10+
*
11+
* More specifically:
12+
* - Any key/value pair in `delta` overrides the corresponding key/value pair in `original`;
13+
* - A key present in `delta` with value `undefined` removes the key from the resulting object;
14+
* - If `original` is `undefined` or empty, return `delta`;
15+
* - If `delta` is `undefined` or empty, return `original` (or undefined if `original` is also undefined);
16+
* - If there are no changes, then return `original`.
17+
*/
18+
export function mergeObjects<T extends Record<string, any>>(original: T, delta: T | undefined): T;
19+
export function mergeObjects<T extends Record<string, any>>(
20+
original: T | undefined,
21+
delta: T | undefined
22+
): T | undefined {
23+
if (original == null) return delta;
24+
if (delta == null) return original;
25+
26+
const merged: Record<string, any> = { ...original };
27+
let changed = false;
28+
for (const [k, v] of Object.entries(delta)) {
29+
if (v !== merged[k]) {
30+
if (v == null) delete merged[k];
31+
else merged[k] = v;
32+
changed = true;
33+
}
34+
}
35+
36+
return changed ? (merged as T) : original;
37+
}

packages/common/src/logger.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { filterNullAndUndefined, mergeObjects } from './internal-workflow';
2+
13
export type LogLevel = 'TRACE' | 'DEBUG' | 'INFO' | 'WARN' | 'ERROR';
24

35
export type LogMetadata = Record<string | symbol, any>;
@@ -53,3 +55,109 @@ export enum SdkComponent {
5355
*/
5456
core = 'core',
5557
}
58+
59+
////////////////////////////////////////////////////////////////////////////////////////////////////
60+
61+
/**
62+
* @internal
63+
* @hidden
64+
*/
65+
export type LogMetaOrFunc = LogMetadata | (() => LogMetadata);
66+
67+
/**
68+
* A logger implementation that adds metadata before delegating calls to a parent logger.
69+
*
70+
* @internal
71+
* @hidden
72+
*/
73+
export class LoggerWithComposedMetadata implements Logger {
74+
/**
75+
* Return a {@link Logger} that adds metadata before delegating calls to a parent logger.
76+
*
77+
* New metadata may either be specified statically as a delta object, or as a function evaluated
78+
* every time a log is emitted that will return a delta object.
79+
*
80+
* Some optimizations are performed to avoid creating unnecessary objects and to keep runtime
81+
* overhead associated with resolving metadata as low as possible.
82+
*/
83+
public static compose(logger: Logger, metaOrFunc: LogMetaOrFunc): Logger {
84+
// Flatten recursive LoggerWithComposedMetadata instances
85+
if (logger instanceof LoggerWithComposedMetadata) {
86+
const contributors = appendToChain(logger.contributors, metaOrFunc);
87+
// If the new contributor results in no actual change to the chain, then we don't need a new logger
88+
if (contributors === undefined) return logger;
89+
return new LoggerWithComposedMetadata(logger.parentLogger, contributors);
90+
} else {
91+
const contributors = appendToChain(undefined, metaOrFunc);
92+
if (contributors === undefined) return logger;
93+
return new LoggerWithComposedMetadata(logger, contributors);
94+
}
95+
}
96+
97+
constructor(
98+
private readonly parentLogger: Logger,
99+
private readonly contributors: LogMetaOrFunc[]
100+
) {}
101+
102+
log(level: LogLevel, message: string, extraMeta?: LogMetadata): void {
103+
this.parentLogger.log(level, message, resolveMetadata(this.contributors, extraMeta));
104+
}
105+
106+
trace(message: string, extraMeta?: LogMetadata): void {
107+
this.parentLogger.trace(message, resolveMetadata(this.contributors, extraMeta));
108+
}
109+
110+
debug(message: string, extraMeta?: LogMetadata): void {
111+
this.parentLogger.debug(message, resolveMetadata(this.contributors, extraMeta));
112+
}
113+
114+
info(message: string, extraMeta?: LogMetadata): void {
115+
this.parentLogger.info(message, resolveMetadata(this.contributors, extraMeta));
116+
}
117+
118+
warn(message: string, extraMeta?: LogMetadata): void {
119+
this.parentLogger.warn(message, resolveMetadata(this.contributors, extraMeta));
120+
}
121+
122+
error(message: string, extraMeta?: LogMetadata): void {
123+
this.parentLogger.error(message, resolveMetadata(this.contributors, extraMeta));
124+
}
125+
}
126+
127+
function resolveMetadata(contributors: LogMetaOrFunc[], extraMeta?: LogMetadata): LogMetadata {
128+
const resolved = {};
129+
for (const contributor of contributors) {
130+
Object.assign(resolved, typeof contributor === 'function' ? contributor() : contributor);
131+
}
132+
Object.assign(resolved, extraMeta);
133+
return filterNullAndUndefined(resolved);
134+
}
135+
136+
/**
137+
* Append a metadata contributor to the chain, merging it with the former last contributor if both are plain objects
138+
*/
139+
function appendToChain(
140+
existingContributors: LogMetaOrFunc[] | undefined,
141+
newContributor: LogMetaOrFunc
142+
): LogMetaOrFunc[] | undefined {
143+
// If the new contributor is an empty object, then it results in no actual change to the chain
144+
if (typeof newContributor === 'object' && Object.keys(newContributor).length === 0) {
145+
return existingContributors;
146+
}
147+
148+
// If existing chain is empty, then the new contributor is the chain
149+
if (existingContributors == null || existingContributors.length === 0) {
150+
return [newContributor];
151+
}
152+
153+
// If both last contributor and new contributor are plain objects, merge them to a single object.
154+
const last = existingContributors[existingContributors.length - 1];
155+
if (typeof last === 'object' && typeof newContributor === 'object') {
156+
const merged = mergeObjects(last, newContributor);
157+
if (merged === last) return existingContributors;
158+
return [...existingContributors.slice(0, -1), merged];
159+
}
160+
161+
// Otherwise, just append the new contributor to the chain.
162+
return [...existingContributors, newContributor];
163+
}

0 commit comments

Comments
 (0)