Skip to content

Commit e3e391e

Browse files
committed
fix(observability): fixed code snippets in TypeScript observability
1 parent f0001b9 commit e3e391e

1 file changed

Lines changed: 140 additions & 124 deletions

File tree

docs/develop/typescript/platform/observability.mdx

Lines changed: 140 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ To set up tracing of Workflows and Activities, use our `opentelemetry-intercepto
4444

4545
```typescript
4646
telemetryOptions: {
47-
metrics: {
48-
prometheus: { bindAddress: '0.0.0.0:9464' },
49-
},
50-
logging: { forward: { level: 'DEBUG' } },
47+
metrics: {
48+
prometheus: { bindAddress: '0.0.0.0:9464' },
5149
},
50+
logging: { forward: { level: 'DEBUG' } },
51+
},
5252
```
5353

5454
## Set up tracing {/* #tracing */}
@@ -96,7 +96,7 @@ To extend the default ([Trace Context](https://github.com/open-telemetry/opentel
9696

9797
Similarly, you can customize the OpenTelemetry `NodeSDK` propagators by following the instructions in the [Initialize the SDK](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-sdk-node#initialize-the-sdk) section of the `README.md` file.
9898

99-
## Log from a Workflow {/* #logging */}
99+
## Set up Logging {/* #logging */}
100100

101101
Logging enables you to record critical information during code execution.
102102
Loggers create an audit trail and capture information about your Workflow's operation.
@@ -231,7 +231,7 @@ import {
231231
makeTelemetryFilterString,
232232
Runtime,
233233
} from '@temporalio/worker';
234-
import winston from 'winston';
234+
import winston, { transports } from 'winston';
235235

236236
const logger = winston.createLogger({
237237
level: 'info',
@@ -240,7 +240,15 @@ const logger = winston.createLogger({
240240
});
241241

242242
Runtime.install({
243-
logger,
243+
logger: new DefaultLogger('INFO', (entry) => {
244+
logger.log({
245+
label: entry.meta?.activityId ? 'activity' : entry.meta?.workflowId ? 'workflow' : 'worker',
246+
level: entry.level.toLowerCase(),
247+
message: entry.message,
248+
timestamp: Number(entry.timestampNanos / 1_000_000n),
249+
...entry.meta,
250+
});
251+
}),
244252
// The following block is optional, but generally desired.
245253
// It allows capturing log messages emitted by the underlying Temporal Core SDK (native code).
246254
// The Telemetry Filter String determine the desired verboseness of messages emitted by the
@@ -254,120 +262,8 @@ Runtime.install({
254262
});
255263
```
256264

257-
{/* FIXME(JWH): Everything below this point must be revisited and moved to a distinct section (Sinks). */}
258-
259-
### Implementing custom Logging-like features based on Workflow Sinks
260-
261-
Sinks enable one-way export of logs, metrics, and traces from the Workflow isolate to the Node.js environment.
262-
263-
{/*
264-
Workflows in Temporal may be replayed from the beginning of their history when resumed. In order for Temporal to recreate the exact state Workflow code was in, the code is required to be fully deterministic. To prevent breaking determinism, in the TypeScript SDK, Workflow code runs in an isolated execution environment and may not use any of the Node.js APIs or communicate directly with the outside world. */}
265-
266-
Sinks are written as objects with methods.
267-
Similar to Activities, they are declared in the Worker and then proxied in Workflow code, and it helps to share types between both.
268-
269-
#### Comparing Sinks and Activities
270-
271-
Sinks are similar to Activities in that they are both registered on the Worker and proxied into the Workflow.
272-
However, they differ from Activities in important ways:
273-
274-
- A sink function doesn't return any value back to the Workflow and cannot be awaited.
275-
- A sink call isn't recorded in the Event History of a Workflow Execution (no timeouts or retries).
276-
- A sink function _always_ runs on the same Worker that runs the Workflow Execution it's called from.
277-
278-
#### Declare the sink interface
279-
280-
Explicitly declaring a sink's interface is optional but is useful for ensuring type safety in subsequent steps:
281-
282-
<!--SNIPSTART typescript-logger-sink-interface-->
283-
[packages/test/src/workflows/log-sink-tester.ts](https://github.com/temporalio/sdk-typescript/blob/main/packages/test/src/workflows/log-sink-tester.ts)
284-
```ts
285-
import type { Sinks } from '@temporalio/workflow';
286-
287-
export interface CustomLoggerSinks extends Sinks {
288-
customLogger: {
289-
info(message: string): void;
290-
};
291-
}
292-
```
293-
<!--SNIPEND-->
294-
295-
#### Implement sinks
296-
297-
Implementing sinks is a two-step process.
298-
299-
Implement and inject the Sink function into a Worker
300-
301-
<!--SNIPSTART typescript-logger-sink-worker-->
302-
[sinks/src/worker.ts](https://github.com/temporalio/samples-typescript/blob/main/sinks/src/worker.ts)
303-
```ts
304-
import { InjectedSinks, Worker } from '@temporalio/worker';
305-
import { MySinks } from './workflows';
306-
307-
async function main() {
308-
const sinks: InjectedSinks<MySinks> = {
309-
alerter: {
310-
alert: {
311-
fn(workflowInfo, message) {
312-
console.log('sending SMS alert!', {
313-
workflowId: workflowInfo.workflowId,
314-
workflowRunId: workflowInfo.runId,
315-
message,
316-
});
317-
},
318-
callDuringReplay: false, // The default
319-
},
320-
},
321-
};
322-
const worker = await Worker.create({
323-
workflowsPath: require.resolve('./workflows'),
324-
taskQueue: 'sinks',
325-
sinks,
326-
});
327-
await worker.run();
328-
console.log('Worker gracefully shutdown');
329-
}
330-
331-
main().catch((err) => {
332-
console.error(err);
333-
process.exit(1);
334-
});
335-
```
336-
<!--SNIPEND-->
337-
338-
- Sink function implementations are passed as an object into [WorkerOptions](https://typescript.temporal.io/api/interfaces/worker.WorkerOptions/#sinks).
339-
- You can specify whether you want the injected function to be called during Workflow replay by setting the `callDuringReplay` option.
340-
341-
#### Proxy and call a sink function from a Workflow
342-
343-
<!--SNIPSTART typescript-logger-sink-workflow-->
344-
[packages/test/src/workflows/log-sample.ts](https://github.com/temporalio/sdk-typescript/blob/main/packages/test/src/workflows/log-sample.ts)
345-
```ts
346-
import * as wf from '@temporalio/workflow';
347-
348-
export async function logSampleWorkflow(): Promise<void> {
349-
wf.log.info('Workflow execution started');
350-
}
351-
```
352-
<!--SNIPEND-->
353-
354-
Some important features of the [InjectedSinkFunction](https://typescript.temporal.io/api/interfaces/worker.InjectedSinkFunction) interface:
355-
356-
- **Injected WorkflowInfo argument:** The first argument of a Sink function implementation is a [`workflowInfo` object](https://typescript.temporal.io/api/interfaces/workflow.WorkflowInfo/) that contains useful metadata.
357-
- **Limited arguments types:** The remaining Sink function arguments are copied between the sandbox and the Node.js environment using the [structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm).
358-
- **No return value:** To prevent breaking determinism, Sink functions cannot return values to the Workflow.
359-
360-
**Advanced: Performance considerations and non-blocking Sinks**
361-
362-
The injected sink function contributes to the overall Workflow Task processing duration.
363-
364-
- 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.
365-
- 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.
366-
367265
### How to provide a custom logger {/* #custom-logger */}
368266

369-
Use a custom logger for logging.
370-
371267
#### Logging in Workers and Clients
372268

373269
The Worker comes with a default logger, which defaults to log any messages with level `INFO` and higher to `STDERR` using `console.error`.
@@ -405,14 +301,26 @@ logger.error('go');
405301
A common logging use case is logging to a file to be picked up by a collector like the [Datadog Agent](https://docs.datadoghq.com/logs/log_collection/nodejs/?tab=winston30).
406302

407303
```ts
408-
import { Runtime } from '@temporalio/worker';
304+
import {
305+
DefaultLogger,
306+
Runtime
307+
} from '@temporalio/worker';
409308
import winston from 'winston';
410309

411-
const logger = winston.createLogger({
412-
level: 'info',
413-
format: winston.format.json(),
414-
transports: [new transports.File({ filename: '/path/to/worker.log' })],
310+
const winstonLogger = winston.createLogger({
311+
level: 'debug',
415312
});
313+
314+
const logger = new DefaultLogger('DEBUG', (entry) => {
315+
winstonLogger.log({
316+
label: 'worker',
317+
level: entry.level.toLowerCase(),
318+
message: entry.message,
319+
timestamp: Number(entry.timestampNanos / 1_000_000n),
320+
...entry.meta,
321+
});
322+
});
323+
416324
Runtime.install({ logger });
417325
```
418326

@@ -528,3 +436,111 @@ async function yourWorkflow() {
528436
upsertSearchAttributes({ CustomIntField: null });
529437
}
530438
```
439+
440+
## Workflow Sinks
441+
442+
Sinks enable one-way export of logs, metrics, and traces from the Workflow isolate to the Node.js environment.
443+
444+
{/*
445+
Workflows in Temporal may be replayed from the beginning of their history when resumed. In order for Temporal to recreate the exact state Workflow code was in, the code is required to be fully deterministic. To prevent breaking determinism, in the TypeScript SDK, Workflow code runs in an isolated execution environment and may not use any of the Node.js APIs or communicate directly with the outside world. */}
446+
447+
Sinks are written as objects with methods.
448+
Similar to Activities, they are declared in the Worker and then proxied in Workflow code, and it helps to share types between both.
449+
450+
### Comparing Sinks and Activities
451+
452+
Sinks are similar to Activities in that they are both registered on the Worker and proxied into the Workflow.
453+
However, they differ from Activities in important ways:
454+
455+
- A sink function doesn't return any value back to the Workflow and cannot be awaited.
456+
- A sink call isn't recorded in the Event History of a Workflow Execution (no timeouts or retries).
457+
- A sink function _always_ runs on the same Worker that runs the Workflow Execution it's called from.
458+
459+
### Declare the sink interface
460+
461+
Explicitly declaring a sink's interface is optional but is useful for ensuring type safety in subsequent steps:
462+
463+
<!--SNIPSTART typescript-logger-sink-interface-->
464+
[packages/test/src/workflows/log-sink-tester.ts](https://github.com/temporalio/sdk-typescript/blob/main/packages/test/src/workflows/log-sink-tester.ts)
465+
```ts
466+
import type { Sinks } from '@temporalio/workflow';
467+
468+
export interface CustomLoggerSinks extends Sinks {
469+
customLogger: {
470+
info(message: string): void;
471+
};
472+
}
473+
```
474+
<!--SNIPEND-->
475+
476+
### Implement sinks
477+
478+
Implementing sinks is a two-step process.
479+
480+
Implement and inject the Sink function into a Worker
481+
482+
<!--SNIPSTART typescript-logger-sink-worker-->
483+
[sinks/src/worker.ts](https://github.com/temporalio/samples-typescript/blob/main/sinks/src/worker.ts)
484+
```ts
485+
import { InjectedSinks, Worker } from '@temporalio/worker';
486+
import { MySinks } from './workflows';
487+
488+
async function main() {
489+
const sinks: InjectedSinks<MySinks> = {
490+
alerter: {
491+
alert: {
492+
fn(workflowInfo, message) {
493+
console.log('sending SMS alert!', {
494+
workflowId: workflowInfo.workflowId,
495+
workflowRunId: workflowInfo.runId,
496+
message,
497+
});
498+
},
499+
callDuringReplay: false, // The default
500+
},
501+
},
502+
};
503+
const worker = await Worker.create({
504+
workflowsPath: require.resolve('./workflows'),
505+
taskQueue: 'sinks',
506+
sinks,
507+
});
508+
await worker.run();
509+
console.log('Worker gracefully shutdown');
510+
}
511+
512+
main().catch((err) => {
513+
console.error(err);
514+
process.exit(1);
515+
});
516+
```
517+
<!--SNIPEND-->
518+
519+
- Sink function implementations are passed as an object into [WorkerOptions](https://typescript.temporal.io/api/interfaces/worker.WorkerOptions/#sinks).
520+
- You can specify whether you want the injected function to be called during Workflow replay by setting the `callDuringReplay` option.
521+
522+
### Proxy and call a sink function from a Workflow
523+
524+
<!--SNIPSTART typescript-logger-sink-workflow-->
525+
[packages/test/src/workflows/log-sample.ts](https://github.com/temporalio/sdk-typescript/blob/main/packages/test/src/workflows/log-sample.ts)
526+
```ts
527+
import * as wf from '@temporalio/workflow';
528+
529+
export async function logSampleWorkflow(): Promise<void> {
530+
wf.log.info('Workflow execution started');
531+
}
532+
```
533+
<!--SNIPEND-->
534+
535+
Some important features of the [InjectedSinkFunction](https://typescript.temporal.io/api/interfaces/worker.InjectedSinkFunction) interface:
536+
537+
- **Injected WorkflowInfo argument:** The first argument of a Sink function implementation is a [`workflowInfo` object](https://typescript.temporal.io/api/interfaces/workflow.WorkflowInfo/) that contains useful metadata.
538+
- **Limited arguments types:** The remaining Sink function arguments are copied between the sandbox and the Node.js environment using the [structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm).
539+
- **No return value:** To prevent breaking determinism, Sink functions cannot return values to the Workflow.
540+
541+
**Advanced: Performance considerations and non-blocking Sinks**
542+
543+
The injected sink function contributes to the overall Workflow Task processing duration.
544+
545+
- 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.
546+
- 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.

0 commit comments

Comments
 (0)