Backend Development 19 min read

Full-Link Tracing in Node.js Applications: Async Hooks and Zone-Context Design

The article details a full‑link tracing system for Node.js that leverages experimental async_hooks to monitor asynchronous resource lifecycles, builds an invoke‑tree to map parent‑child relationships, implements garbage collection, and provides a ZoneContext API for propagating custom tracing data across async call chains.

vivo Internet Technology
vivo Internet Technology
vivo Internet Technology
Full-Link Tracing in Node.js Applications: Async Hooks and Zone-Context Design

The article explains the two core elements of full‑link tracing: full‑link information acquisition and full‑link information storage and display . It focuses on Node.js applications, which face challenges in acquiring and correlating asynchronous request data.

Two typical Node.js architecture patterns are described (generic SSR/BFF only, and full‑scenario with servers and micro‑services). The need for a technique that aggregates key request information across long request chains and many micro‑service calls is highlighted.

The chosen solution is to use Node.js async_hooks (available since v8.x) to track the lifecycle of asynchronous resources. The article notes that async_hooks is still experimental (Stability: 1) and should not be used in production without caution.

Async Hooks Overview

Async Hooks provides an API to monitor async resource creation, execution, and destruction. A single line imports the module:

import asyncHook from 'async_hooks'

Key concepts include asyncId , triggerAsyncId , and the ability to register hooks via asyncHook.createHook .

Design of the Full‑Link Tracing System

The system consists of three main functions:

Asynchronous resource listening (using asyncHook.createHook )

Invoke tree – a data structure that records parent‑child relationships of async resources

Garbage collection (gc) to clean up the invoke tree when resources finish

Async Resource Listening Code

asyncHook
  .createHook({
    init(asyncId, type, triggerAsyncId) {
      // Called when an async resource is created
    },
  })
  .enable()

Invoke Tree Structure

interface ITree {
  [key: string]: {
    // asyncId of the first async resource in the call chain
    rootId: number
    // triggerAsyncId of the async resource
    pid: number
    // asyncIds of child async resources
    children: Array
}
}

const invokeTree: ITree = {}

The init hook links asyncId and triggerAsyncId into the invokeTree :

asyncHook
  .createHook({
    init(asyncId, type, triggerAsyncId) {
      const parent = invokeTree[triggerAsyncId]
      if (parent) {
        invokeTree[asyncId] = {
          pid: triggerAsyncId,
          rootId: parent.rootId,
          children: [],
        }
        invokeTree[triggerAsyncId].children.push(asyncId)
      }
    }
  })
  .enable()

Garbage Collection Design

When an async resource ends, the gc function recursively collects all descendant ids and removes them from invokeTree and the root map.

interface IRoot {
  [key: string]: Object
}

const root: IRoot = {}

function gc(rootId: number) {
  if (!root[rootId]) return
  const collectionAllNodeId = (rootId: number) => {
    const { children } = invokeTree[rootId]
    let allNodeId = [...children]
    for (let id of children) {
      allNodeId = [...allNodeId, ...collectionAllNodeId(id)]
    }
    return allNodeId
  }
  const allNodes = collectionAllNodeId(rootId)
  for (let id of allNodes) {
    delete invokeTree[id]
  }
  delete invokeTree[rootId]
  delete root[rootId]
}

Zone‑Context API

Three helper functions are provided to create a scoped async resource, set additional tracing data, and retrieve it:

ZoneContext(fn) – creates an AsyncResource , records the root id, and runs the user function within that async scope.

setZoneContext(obj) – merges custom data into the root context.

getZoneContext() – fetches the stored root context for the current async id.

// ZoneContext factory
async function ZoneContext(fn: Function) {
  const asyncResource = new asyncHook.AsyncResource('ZoneContext')
  let rootId = -1
  return asyncResource.runInAsyncScope(async () => {
    try {
      rootId = asyncHook.executionAsyncId()
      root[rootId] = {}
      invokeTree[rootId] = { pid: -1, rootId, children: [] }
      await fn()
    } finally {
      gc(rootId)
    }
  })
}

function setZoneContext(obj: Object) {
  const curId = asyncHook.executionAsyncId()
  let root = findRootVal(curId)
  Object.assign(root, obj)
}

function findRootVal(asyncId: number) {
  const node = invokeTree[asyncId]
  return node ? root[node.rootId] : null
}

function getZoneContext() {
  const curId = asyncHook.executionAsyncId()
  return findRootVal(curId)
}

Demo Usage

A demonstration shows how async functions A , B , and C are traced, with console output of async ids and the constructed invoke tree. The demo also illustrates setting custom tracing information via setZoneContext and retrieving it in nested async calls.

// Example tracing demo
ZoneContext(async () => {
  await A()
})

async function A() {
  fs.writeSync(1, `A asyncId -> ${asyncHook.executionAsyncId()}\n`)
  Promise.resolve().then(() => {
    fs.writeSync(1, `A promise asyncId -> ${asyncHook.executionAsyncId()}\n`)
    B()
  })
}

async function B() {
  fs.writeSync(1, `B asyncId -> ${asyncHook.executionAsyncId()}\n`)
  Promise.resolve().then(() => {
    fs.writeSync(1, `B promise asyncId -> ${asyncHook.executionAsyncId()}\n`)
    C()
  })
}

function C() {
  const ctx = getZoneContext()
  fs.writeSync(1, `C context -> ${JSON.stringify(ctx)}\n`)
}

The output confirms the nesting relationship A → B → C and shows that the custom context set at the top level is accessible in both C and any synchronous functions called later.

In summary, the article presents a complete design and implementation for acquiring full‑link information in Node.js applications using async_hooks , an invoke‑tree data structure, garbage collection, and a convenient ZoneContext API. The next article will cover storage and visualization of the collected data based on the OpenTracing specification.

Node.jsGarbage Collectionperformance-monitoringfull-link tracingasync_hooksZone Context
vivo Internet Technology
Written by

vivo Internet Technology

Sharing practical vivo Internet technology insights and salon events, plus the latest industry news and hot conferences.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.