How to log from a Workflow in TypeScript
Logging from Workflows is tricky for two reasons:
- Workflows run in a sandboxed environment and cannot do any I/O.
- 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
- 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
- TypeScript
- JavaScript
import { Sinks } from '@temporalio/workflow';
export interface LoggerSinks extends Sinks {
logger: {
info(message: string): void;
};
}
// Not required in JavaScript
Implementing Sinks
Implementing Sinks is a two-step process.
Implement and inject the Sink function into a Worker
- TypeScript
- JavaScript
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);
}
);
import { Worker } from '@temporalio/worker';
async function main() {
const sinks = {
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
- TypeScript
- JavaScript
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');
}
import * as wf from '@temporalio/workflow';
const { logger } = wf.proxySinks();
export async function logSampleWorkflow() {
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.