Observability with diagnostics_channel and AsyncLocalStorage

Rate this content
Bookmark
The video explores the use of Diagnostics Channel and AsyncLocalStorage for enhancing observability in Node.js applications. Diagnostics Channel acts as a high-performance global event channel, allowing developers to broadcast data with minimal performance impact. Channels can be created and subscribed to anywhere in the application. AsyncLocalStorage, on the other hand, provides a way to store data across asynchronous operations, following the call path rather than the scope of definition. This enables developers to propagate values through calls, callbacks, and promise continuations without passing them as parameters. The video also covers how to use Diagnostics Channel with AsyncLocalStorage for tracing and logging, ensuring efficient observability without direct inter-module dependencies. The span object, used for storing data about the current execution, is highlighted as a key component for tracing asynchronous tasks. The combination of Diagnostics Channel and AsyncLocalStorage supports detailed tracking of application behavior and error handling in Node.js.

From Author:

Modern tracing products work by combining diagnostics_channel with AsyncLocalStorage. Let's build a tracer together to see how it works and what you can do to make your apps more observable.

This talk has been presented at Node Congress 2023, check out the latest edition of this JavaScript Conference.

FAQ

Diagnostics Channel is a high-performance global event channel designed for passively broadcasting data about the current execution in Node.js applications. It functions similarly to an event emitter but is optimized to be low-cost when no listeners are active, encouraging developers to broadcast data without worrying about performance impact.

To create a Diagnostics Channel, call the 'channel' function at the top of your JavaScript file and provide a name for your channel. You can then publish to this channel using the 'publish' function. Channels share a global namespace, allowing you to acquire the same channel anywhere in your application without explicitly sharing the channel object.

Async local storage in Node.js provides a way to store data that follows the call path rather than the scope of definition, similar to lexical scoping. It enables data to be passed through asynchronous callbacks and promise continuations without having to explicitly pass it through function parameters.

To implement async local storage, use the 'run' method to pass values through the current state of the store during function execution. Values can later be retrieved using the 'get store' method, ensuring that data flows with the execution context across asynchronous operations.

Combining Diagnostics Channel with async local storage allows for decoupled code and detailed tracing of application behavior without direct inter-module dependencies. This setup supports efficient observability and debugging in Node.js applications by allowing passive observation and context tracking across asynchronous tasks.

Diagnostics Channel is designed to be as low-cost as possible when no subscribers are listening. This design allows developers to broadcast extensive data without significant performance penalties, optimizing the observability of applications without degrading their operational efficiency.

Yes, you can subscribe to channels for modules that are never loaded, which means events will never be published from these modules. This feature is particularly useful for tracing products designed to observe module behavior passively, without requiring an explicit connection between modules.

Stephen Belanger
Stephen Belanger
21 min
17 Apr, 2023

Comments

Sign in or register to post your comment.

Video Transcription

1. Introduction to Diagnostics Channel

Short description:

Let's talk about observability with Diagnostics Channel and async local storage. Diagnostics Channel is a high-performance global event channel designed to be low-cost when nothing is actively listening. Channels are created at the top level of your JavaScript file and can be subscribed to from anywhere in the application. Each diagnostic channel should have a single object structure, and you can subscribe to channels for modules that are never loaded. An example is provided to demonstrate how to use the channel and publish data to it.

So let's talk about observability with Diagnostics Channel and async local storage. So hi there. I'm Steven. I've been involved in Node.js core in a diagnostics working group for many years. I work at Datadog building diagnostics tools and my pronouns are he-him.

So first of all, what is Diagnostics Channel? So Diagnostics Channel is high-performance global event channel to passively broadcast data about the current execution. It's a lot like an event emitter, but specifically designed to be as low-cost as possible when nothing is actively listening. The idea being that users should feel comfortable broadcasting lots of things without worrying about the cost, if it's not going to be observed most of the time.

So channels are created at the top level of your JavaScript file by calling the channel function and providing a name for your channel. share a global namespace to allow acquiring the same channel anywhere in the application without needing to explicitly share the channel object, and your module name should generally be included in the name to avoid collisions with channels from other things. Once you have the channel object, you can start publishing to it. This is like the emit function on an event emitter but by creating channel objects ahead of time, it allows several optimizations such as avoiding looking up the handle each by name every time an event occurs, and making a publish call as close to zero cost as possible when there are no listeners.

So each diagnostic channel should follow a convention of having a single object structure per channel, and if you have differently shaped data sets to communicate it's likely those should probably be separate channels. So at least document the names and shapes of your channels. Anywhere in the application you can call channel again with the same name to acquire the same channel and then subscribe to it. The order of channel calls doesn't matter. Whichever place calls it first will create the channel, and every subsequent call will retrieve it. You can even subscribe to channels for modules that are never loaded and therefore never have events published. This enables complete decoupling of code, allowing things like tracing products to passively observe your module behavior without needing any explicit connection between modules.

So let's look at an example. We start by defining our named channel at the top of the file. Then we write our function, which we want to broadcast some data about. Typically when it gets called, like it completes its internal set immediate, and calls its callback, it will broadcast the data to the channel with the publish function. This could be handy for all sorts of things. For example, you might want to capture metrics of how many times do a thing did whatever it was supposed to do. It could even be captured with the time of completion to chart activity over time. None of this needs to be specifically supported by DoA Thing as the subscriber can decide on its own what to do with the message or if it wants to capture any timing information. Publish is no op unless there are subscribers. It has like no cost unless there are subscribers. Sometimes constructing the message itself can be costly if the thing would run very frequently so there's the has subscribers can be used to skip message construction entirely in performance critical situations.

2. Understanding Async Local Storage

Short description:

Async local storage is like lexical scope but follows the call path instead of the scope of definition. It allows us to propagate values through calls, callbacks, and promise continuations without needing to pass them as parameters. To use async local storage, you call the run method on the store to pass the value and retrieve it later with the get store method.

So what is async local storage? It's like lexical scope but following the call path rather than the scope of definition. Lexical scope allows us to retrieve data from the scope outside of the function itself. However, when a calls b and we define 'thing' in an inner scope, there's no way to reach it in b. In more complicated situations where we don't control the intermediate steps between where we've defined the variable and where we want to access it, passing it as a parameter to b is not feasible. So how can we get 'thing' into the event handler of 'something' when we can't pass it through the interface? This is where async local storage comes in. With async local storage, the value gets propagated through calls automatically without needing to add it to the arguments, and it flows into callbacks and promise continuations. This means that as long as a call, callback, or continuation path can be drawn between the two points, the value should be available. To pass the value through, you call the run method on the store, which will call the given function with that value as the current state of the store. And to retrieve the value from the store, you can call the get store method later.