Skip to main content

How to log from a Workflow in TypeScript

Logging from Workflows is tricky for two reasons:

  1. Workflows run in a sandboxed environment and cannot do any I/O.
  2. Workflow code might get replayed at any time, generating duplicate log messages.

To work around these limitations, we recommend using the Sinks feature in the TypeScript SDK. Sinks enable one-way export of logs, metrics, and traces from the Workflow isolate to the Node.js environment.

Sinks are written as objects with methods. Similar to Activities, they are declared in the Worker and then proxied in Workflow code, and it helps to share types between both.

Comparing Sinks, Activities and Interceptors
Sinks are similar to Activities in that they are both registered on the Worker and proxied into the Workflow. However, they differ from Activities in important ways:
  • Sink functions don't return any value back to the Workflow and cannot not be awaited.
  • Sink calls are not recorded in Workflow histories (no timeouts or retries).
  • Sink functions are always run on the same Worker that runs the Workflow they are called from.

Declaring the Sink Interface

Explicitly declaring a Sink's interface is optional, but is useful for ensuring type safety in subsequent steps:

packages/test/src/workflows/definitions.ts

import { Sinks } from '@temporalio/workflow';

export interface LoggerSinks extends Sinks {
logger: {
info(message: string): void;
};
}

Implementing Sinks

Implementing Sinks is a two-step process.

Implement and inject the Sink function into a Worker

logging-sinks/src/worker.ts

import { Worker, InjectedSinks } from '@temporalio/worker';
import { LoggerSinks } from './workflows';

async function main() {
const sinks: InjectedSinks<LoggerSinks> = {
logger: {
info: {
fn(workflowInfo, message) {
console.log('workflow: ', workflowInfo.runId, 'message: ', message);
},
callDuringReplay: false, // The default
},
},
};
const worker = await Worker.create({
workflowsPath: require.resolve('./workflows'),
taskQueue: 'logging-sinks',
sinks,
});
await worker.run();
console.log('Worker gracefully shutdown');
}

main().then(
() => void process.exit(0),
(err) => {
console.error(err);
process.exit(1);
}
);
  • Sink function implementations are passed as an object into WorkerOptions
  • You can specify whether you want the injected function to be called during Workflow replay by setting the callDuringReplay boolean option.

Proxy and call a Sink function from a Workflow

packages/test/src/workflows/log-sample.ts

import * as wf from '@temporalio/workflow';
import { LoggerSinks } from './definitions';

const { logger } = wf.proxySinks<LoggerSinks>();

export async function logSampleWorkflow(): Promise<void> {
logger.info('Workflow execution started');
}

Some important features of the InjectedSinkFunction interface:

  • Injected WorkflowInfo argument: The first argument of a Sink function implementation is a workflowInfo object that contains useful metadata.
  • Limited arguments types: The remaining Sink function arguments are copied between the sandbox and the Node.js environment using the structured clone algorithm.
  • No return value: To prevent breaking determinism, Sink functions cannot return values to the Workflow.

Advanced: Performance considerations and non-blocking Sinks

The injected sink function contributes to the overall Workflow Task processing duration.

  • If you have a long-running sink function, such as one that tries to communicate with external services, you might start seeing Workflow Task timeouts.
  • The effect is multiplied when using callDuringReplay: true and replaying long Workflow histories because the Workflow Task timer starts when the first history page is delivered to the Worker.