Skip to content
30 changes: 30 additions & 0 deletions contrib/interceptors-opentelemetry-v2/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,18 @@ import type {
WorkflowTerminateInput,
WorkflowCancelInput,
WorkflowDescribeInput,
WorkflowFetchHistoryInput,
WorkflowListInput,
WorkflowClientInterceptor,
TerminateWorkflowExecutionResponse,
RequestCancelWorkflowExecutionResponse,
DescribeWorkflowExecutionResponse,
WorkflowExecutionInfo,
} from '@temporalio/client';
import type { History } from '@temporalio/common/lib/proto-utils';
import {
instrument,
instrumentSync,
headersWithContext,
RUN_ID_ATTR_KEY,
WORKFLOW_ID_ATTR_KEY,
Expand Down Expand Up @@ -217,4 +222,29 @@ export class OpenTelemetryWorkflowClientInterceptor implements WorkflowClientInt
},
});
}

async fetchHistory(
input: WorkflowFetchHistoryInput,
next: Next<WorkflowClientInterceptor, 'fetchHistory'>
): Promise<History> {
return await instrument({
tracer: this.tracer,
spanName: SpanName.WORKFLOW_FETCH_HISTORY,
fn: async (span) => {
span.setAttribute(WORKFLOW_ID_ATTR_KEY, input.workflowExecution.workflowId);
if (input.workflowExecution.runId) {
span.setAttribute(RUN_ID_ATTR_KEY, input.workflowExecution.runId);
}
return await next(input);
},
});
}

list(input: WorkflowListInput, next: Next<WorkflowClientInterceptor, 'list'>): AsyncIterable<WorkflowExecutionInfo> {
return instrumentSync({
tracer: this.tracer,
spanName: SpanName.WORKFLOW_LIST,
fn: () => next(input),
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,16 @@ export enum SpanName {
*/
WORKFLOW_DESCRIBE = 'DescribeWorkflow',

/**
* Workflow history is fetched
*/
WORKFLOW_FETCH_HISTORY = 'FetchWorkflowHistory',

/**
* Workflows are listed
*/
WORKFLOW_LIST = 'ListWorkflows',

/**
* Workflow run is executing
*/
Expand Down
30 changes: 30 additions & 0 deletions contrib/interceptors-opentelemetry/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,18 @@ import type {
WorkflowTerminateInput,
WorkflowCancelInput,
WorkflowDescribeInput,
WorkflowFetchHistoryInput,
WorkflowListInput,
WorkflowClientInterceptor,
TerminateWorkflowExecutionResponse,
RequestCancelWorkflowExecutionResponse,
DescribeWorkflowExecutionResponse,
WorkflowExecutionInfo,
} from '@temporalio/client';
import type { History } from '@temporalio/common/lib/proto-utils';
import {
instrument,
instrumentSync,
headersWithContext,
RUN_ID_ATTR_KEY,
WORKFLOW_ID_ATTR_KEY,
Expand Down Expand Up @@ -215,4 +220,29 @@ export class OpenTelemetryWorkflowClientInterceptor implements WorkflowClientInt
},
});
}

async fetchHistory(
input: WorkflowFetchHistoryInput,
next: Next<WorkflowClientInterceptor, 'fetchHistory'>
): Promise<History> {
return await instrument({
tracer: this.tracer,
spanName: SpanName.WORKFLOW_FETCH_HISTORY,
fn: async (span) => {
span.setAttribute(WORKFLOW_ID_ATTR_KEY, input.workflowExecution.workflowId);
if (input.workflowExecution.runId) {
span.setAttribute(RUN_ID_ATTR_KEY, input.workflowExecution.runId);
}
return await next(input);
},
});
}

list(input: WorkflowListInput, next: Next<WorkflowClientInterceptor, 'list'>): AsyncIterable<WorkflowExecutionInfo> {
return instrumentSync({
tracer: this.tracer,
spanName: SpanName.WORKFLOW_LIST,
fn: () => next(input),
});
}
}
10 changes: 10 additions & 0 deletions contrib/interceptors-opentelemetry/src/workflow/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,16 @@ export enum SpanName {
*/
WORKFLOW_DESCRIBE = 'DescribeWorkflow',

/**
* Workflow history is fetched
*/
WORKFLOW_FETCH_HISTORY = 'FetchWorkflowHistory',

/**
* Workflows are listed
*/
WORKFLOW_LIST = 'ListWorkflows',

/**
* Workflow run is executing
*/
Expand Down
21 changes: 21 additions & 0 deletions packages/client/src/interceptors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import type { Duration, SearchAttributePair, TypedSearchAttributes } from '@temporalio/common';
import { Headers, Next } from '@temporalio/common';
import type { History } from '@temporalio/common/lib/proto-utils';
import type { temporal } from '@temporalio/proto';
import type { NexusOperationHandle } from './nexus-client';
import type {
Expand All @@ -24,6 +25,7 @@ import type {
RequestCancelWorkflowExecutionResponse,
TerminateWorkflowExecutionResponse,
WorkflowExecution,
WorkflowExecutionInfo,
} from './types';
import type { CompiledWorkflowOptions, WorkflowUpdateOptions } from './workflow-options';
import type { ActivityHandle, ActivityOptions } from './activity-client';
Expand Down Expand Up @@ -128,6 +130,17 @@ export interface WorkflowDescribeInput {
readonly workflowExecution: WorkflowExecution;
}

/** Input for WorkflowClientInterceptor.fetchHistory */
export interface WorkflowFetchHistoryInput {
readonly workflowExecution: WorkflowExecution;
}

/** Input for WorkflowClientInterceptor.list */
export interface WorkflowListInput {
readonly query?: string;
readonly pageSize?: number;
}

/**
* Implement any of these methods to intercept {@link WorkflowClient} outbound calls
*
Expand Down Expand Up @@ -198,6 +211,14 @@ export interface WorkflowClientInterceptor {
* Intercept a service call to describeWorkflowExecution
*/
describe?: (input: WorkflowDescribeInput, next: Next<this, 'describe'>) => Promise<DescribeWorkflowExecutionResponse>;
/**
* Intercept a service call to getWorkflowExecutionHistory
*/
fetchHistory?: (input: WorkflowFetchHistoryInput, next: Next<this, 'fetchHistory'>) => Promise<History>;
/**
* Intercept a service call to listWorkflowExecutions
*/
list?: (input: WorkflowListInput, next: Next<this, 'list'>) => AsyncIterable<WorkflowExecutionInfo>;
}

/** @deprecated: Use {@link WorkflowClientInterceptor} instead */
Expand Down
62 changes: 43 additions & 19 deletions packages/client/src/workflow-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ import type {
WorkflowClientInterceptor,
WorkflowClientInterceptors,
WorkflowDescribeInput,
WorkflowFetchHistoryInput,
WorkflowListInput,
WorkflowQueryInput,
WorkflowSignalInput,
WorkflowSignalWithStartInput,
Expand Down Expand Up @@ -1568,6 +1570,28 @@ export class WorkflowClient extends BaseClient {
}
}

/**
* Uses given input to make getWorkflowExecutionHistory call(s) to the service
*
* Used as the final function of the fetchHistory interceptor chain
*/
protected async _fetchHistoryHandler(input: WorkflowFetchHistoryInput): Promise<History> {
let nextPageToken: Uint8Array | undefined = undefined;
const events = Array<temporal.api.history.v1.IHistoryEvent>();
for (;;) {
const response: temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryResponse =
await this.workflowService.getWorkflowExecutionHistory({
nextPageToken,
namespace: this.options.namespace,
execution: input.workflowExecution,
});
events.push(...(response.history?.events ?? []));
nextPageToken = response.nextPageToken;
if (nextPageToken == null || nextPageToken.length === 0) break;
}
return temporal.api.history.v1.History.create({ events });
}

/**
* Create a new workflow handle for new or existing Workflow execution
*/
Expand Down Expand Up @@ -1656,20 +1680,11 @@ export class WorkflowClient extends BaseClient {
};
},
async fetchHistory() {
let nextPageToken: Uint8Array | undefined = undefined;
const events = Array<temporal.api.history.v1.IHistoryEvent>();
for (;;) {
const response: temporal.api.workflowservice.v1.GetWorkflowExecutionHistoryResponse =
await this.client.workflowService.getWorkflowExecutionHistory({
nextPageToken,
namespace: this.client.options.namespace,
execution: { workflowId, runId },
});
events.push(...(response.history?.events ?? []));
nextPageToken = response.nextPageToken;
if (nextPageToken == null || nextPageToken.length === 0) break;
}
return temporal.api.history.v1.History.create({ events });
const next = this.client._fetchHistoryHandler.bind(this.client);
const fn = composeInterceptors(interceptors, 'fetchHistory', next);
return await fn({
workflowExecution: { workflowId, runId },
});
},
async startUpdate<Ret, Args extends any[]>(
def: UpdateDefinition<Ret, Args> | string,
Expand Down Expand Up @@ -1753,16 +1768,16 @@ export class WorkflowClient extends BaseClient {
});
}

protected async *_list(options?: ListOptions): AsyncIterable<WorkflowExecutionInfo> {
protected async *_list(input: WorkflowListInput): AsyncIterable<WorkflowExecutionInfo> {
let nextPageToken: Uint8Array = Buffer.alloc(0);
for (;;) {
let response: temporal.api.workflowservice.v1.ListWorkflowExecutionsResponse;
try {
response = await this.workflowService.listWorkflowExecutions({
namespace: this.options.namespace,
query: options?.query,
query: input.query,
nextPageToken,
pageSize: options?.pageSize,
pageSize: input.pageSize,
});
} catch (e) {
this.rethrowGrpcError(e, 'Failed to list workflows', undefined);
Expand All @@ -1787,11 +1802,20 @@ export class WorkflowClient extends BaseClient {
* https://docs.temporal.io/visibility
*/
public list(options?: ListOptions): AsyncWorkflowListIterable {
const input: WorkflowListInput = {
query: options?.query,
pageSize: options?.pageSize,
};
// Deprecated interceptor factories require a workflowId and are skipped for client-level list.
const interceptors = Array.isArray(this.options.interceptors)
? (this.options.interceptors as WorkflowClientInterceptor[])
: [];
const list = composeInterceptors(interceptors, 'list', this._list.bind(this));
return {
[Symbol.asyncIterator]: () => this._list(options)[Symbol.asyncIterator](),
[Symbol.asyncIterator]: () => list(input)[Symbol.asyncIterator](),
intoHistories: (intoHistoriesOptions?: IntoHistoriesOptions) => {
return mapAsyncIterable(
this._list(options),
list(input),
async ({ workflowId, runId }) => ({
workflowId,
history: await this.getHandle(workflowId, runId).fetchHistory(),
Expand Down
41 changes: 41 additions & 0 deletions packages/test/src/test-interceptors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,47 @@ if (RUN_INTEGRATION_TESTS) {
});
});

test.serial('WorkflowClientInterceptor intercepts list and fetchHistory', async (t) => {
const taskQueue = 'test-interceptor-list-and-fetch-history';
const workflowId = randomUUID();
const worker = await Worker.create({
...defaultOptions,
taskQueue,
});
let listCalls = 0;
let fetchHistoryCalls = 0;
const client = new WorkflowClient({
interceptors: [
{
list(input, next) {
listCalls += 1;
return next(input);
},
async fetchHistory(input, next) {
fetchHistoryCalls += 1;
return next(input);
},
},
],
});

await worker.runUntil(async () => {
await client.execute(successString, {
taskQueue,
workflowId,
});

const history = await client.getHandle(workflowId).fetchHistory();
t.true((history.events?.length ?? 0) > 0);
t.is(fetchHistoryCalls, 1);

for await (const _ of client.list({ query: `WorkflowId = "${workflowId}"` })) {
// consume iterator
}
t.is(listCalls, 1);
});
});

test.serial('Workflow continueAsNew can be intercepted', async (t) => {
const taskQueue = 'test-continue-as-new-interceptor';
const worker = await Worker.create({
Expand Down