11/10/23

Tracing TypeScript's Trails

The wizardry of AsyncLocalStorage

6 Min Read

Since the beginning, Encore has focused on improving the developer experience when building distributed backend systems. One of the core aspects of this is the transparent and fully automatic tracing we provide both in your deployed app and when you are developing it locally. This gives you complete visibility into what's happening within your application without setting up a trace collector and storage or manually tracking spans all over your codebase.

This post aims to take you on a journey through the intricacies of our TypeScript request tracking implementation, comparing it with our Go approach and highlighting the benefits users can expect. While the languages differ, our commitment remains unwavering: to deliver a seamless developer experience, regardless of the underlying tech.

Request Tracking in Go – A Quick Glance

When building an application on Encore with Go, most developers seldom think about tracing until you see traces appearing in your local dashboard - and yet when you get an annoying bug, they are there instantly and allow you to deep dive into the problem, inspecting the request/response objects of any of the API calls made deep within the stack as well as the database queries made.

This has been achieved through a two-fold approach; first, our API and Infrastructure SDKs are deeply integrated into our tracing system, allowing them to provide rich span data and then from the moment a trace is started, either on a request to the HTTP server or a Pub/Sub message is received, we trace the go-routines involved until the point the request and any spawned work is completed (I recently gave a talk at GopherCon on how we do this, which you can watch here).

In essence, when a request starts, we generate a Request object, which we effectively track against the processing of the entire request at each significant point in the application (API calls to other services, external HTTP calls, database calls and pub/sub publishes) we automatically create new spans in the trace and emit events for those spans.

The tracing system running within our SDK then streams events in real time to our trace engine, which indexes & stores the events, and it is from this data store the dashboard is able to show you what's happened inside your app.

The beauty of Encore's Go-based request tracking lies in its seamless integration and automatic trace generation. Developers don't have to wrestle with the complexities of tracing; it just works right out of the box. With a combination of deep SDK integrations, real-time event streaming, and a robust trace engine, Encore ensures that Go developers have all the tools at their disposal for a transparent view into their applications. If you thought Go's request tracking was magic, wait till you see how we conjured the same for TypeScript

Diving into TypeScript

As we started building our TypeScript SDK, we aimed to cast a wide net to cater to today's diverse JavaScript runtimes, such as Node.js, Deno, and Bun. Ensuring compatibility across these runtimes is a strategic move to future-proof Encore, more than just meeting present needs. By acknowledging the diverse ecosystem and the constant evolution of JavaScript runtimes, we're building a resilient and adaptive foundation for future changes. This was achieved by introducing adapter packages for each runtime, which provides a unified interface to our core SDK, which allows that core to become agnostic to the runtime it's running on.

Unlike Go, which has a standardised way of passing information through its context, JavaScript presents a challenging landscape. The language's flexibility, often its strength, can lead to divergent practices. In JavaScript, developers are accustomed to passing information in myriad ways: callbacks, promises, or even asynchronous operations like setTimeout and process.nextTick to handle asynchronous operations. While providing flexibility, this diversity necessitates a robust and unifying approach to maintain consistent request tracking.

Tracking a request's journey becomes complex, given the numerous ways to handle events and data flow in JavaScript. The challenge is maintaining context across various asynchronous operations, whether callback-driven or promise-based. A tracking system should not just work but should be agnostic to the intricacies of how the code is executed.

Enter AsyncLocalStorage: Consistency in Chaos

To address these challenges, Encore's TypeScript SDK leverages AsyncLocalStorage. This API is part of the async_hooks module in Node.js, and has been standardised into both Deno and Bun. It is designed to create, manage, and propagate context across asynchronous boundaries; regardless of whether you're using callbacks, promises, or timers, AsyncLocalStorage ensures that the context stays consistent.

With AsyncLocalStorage, we create a context for every incoming request and seamlessly propagate it through the various asynchronous events the request triggers. This approach mirrors the simplicity and effectiveness we achieved with Go but is tailored to the quirks and flexibilities of JavaScript.

Its API design is effortless yet very powerful:

import { AsyncLocalStorage } from "node:async_hooks"; // First, we create a singleton instance of the AsyncLocalStorage object const storage = new AsyncLocalStorage<string>(); // When we call it outside a run, we get undefined console.log("value is ", storage.getStore()) // "value is undefined" // We can then run some code with a value set storage.run("foo", () => { // And within the block, the value is set console.log("value is ", storage.getStore()) // "value is foo" setTimeout(() => { // Even if we asynchronously read it after the original function // has exited console.log("value is ", storage.getStore()) // "value is foo" }, 2000) }) // However, the value is still undefined here, even though the // timeout won't have executed yet console.log("value is ", storage.getStore()) // "value is undefined"

By storing a more complex object with the trace ID, span ID and other metadata about the request, we can ensure that no matter what the application code does, the Encore SDK can track and record spans on behalf of the developer without them needing to pass information around.

A Tale of Two Languages

While Go and TypeScript present their own sets of challenges and nuances, our mission at Encore remains consistent: to provide developers an unparalleled, seamless experience. Through deep integration, real-time streaming, and the magic of AsyncLocalStorage, we ensure that developers, regardless of their chosen language, have a transparent lens into their application's soul. Dive in, explore, and let Encore's magic be a part of your developer journey.

Coming to an IDE near you soon

From this post, we're clearly in the thick of expanding Encore's reach to the TypeScript community. By the close of 2023, we're launching our TypeScript beta. Moreover, we're crafting a platform where you can intertwine TypeScript and Go services in your application. These services won't just coexist; they'll call each other seamlessly, tap into shared infrastructure resources, and deploy together with a single git push.

This isn't just about versatility—it's about empowerment. We believe in giving you the freedom to choose the best language for each component of your application. No more compromising or being boxed into one language choice.

Eager to be at the forefront of this evolution? For early access to our TypeScript beta and a chance to help shape this journey, join us on Discord and drop us a line.

About The Author

Dominic is a founding engineer at Encore with a decades-long background in building large-scale distributed systems at fintechs like Monzo.

Dominic regularly writes about Go, distributed systems, and backend development. If you enjoyed this blog post, you should sign up for our newsletter to make sure you don't miss future posts.

Encore

This blog is presented by Encore, the Development Platform for startups building event-driven and distributed systems.

Like this article?
Get future ones straight to your mailbox.

You can unsubscribe at any time.