npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cieljs/instrument

v0.0.1

Published

轻量, 类型安全且可组合的 TypeScript 函数插桩工具, 为日志, 遥测, Tracing, DevTools 与运行时观测提供统一的拦截基础

Readme

@cieljs/instrument

轻量、类型安全且可组合的 TypeScript 函数插桩工具。

为日志、遥测、Tracing、DevTools 与运行时观测提供统一的拦截基础。


我们经常希望知道一个函数:

  • 什么时候开始执行?
  • 接收了什么参数?
  • 返回了什么结果?
  • 执行花了多长时间?
  • 为什么失败?
  • 属于哪个模块、插件或能力?
  • 应该如何被 DevTools、Logger 或 Telemetry 系统观察?

最直接的办法,是把这些逻辑写进业务代码。

async function searchMemory(query: string) {
  const startedAt = performance.now();

  try {
    logger.debug('memory.search:start', {
      query,
    });

    const result = await memory.search(query);

    logger.debug('memory.search:end', {
      duration: performance.now() - startedAt,
    });

    return result;
  } catch (error) {
    logger.error('memory.search:error', {
      error,
    });

    throw error;
  }
}

但随着观测能力增加,业务函数会逐渐承担越来越多与自身无关的职责。

@cieljs/instrument 希望提供另一种可能:

让函数保持函数本身,让观测留在函数之外。

它提供一个很小的 instrumentation primitive,通过 Instrument、Interceptor 和可派生的 Context,将横切逻辑组合到普通 TypeScript 函数之外。

Features

  • 🪶 Lightweight — 很小的运行时抽象,不绑定任何日志、遥测或 Tracing 实现
  • 🔒 Type-safe — 包装前后完整保留函数参数与返回值类型
  • 🧩 Composable — 多个 Interceptor 可以按照声明顺序组合
  • 🌲 Derived Context — 使用 .with() 分层绑定 Instrument 上下文
  • 🛡️ Owner Priority — 上层声明的身份信息不会被下层调用方覆盖
  • 🎯 Selective Interception — Interceptor 可以自行判断是否需要处理目标
  • ♻️ Reusable — 匹配过程只发生在 instrument 阶段,包装结果可被重复调用
  • 🫧 Transparent — 保留原始 this、同步返回、Promise 和异常语义
  • 🔌 Framework Agnostic — 不依赖框架,可用于 Node.js、Bun、浏览器或任意 TypeScript Runtime

Installation

vp add @cieljs/instrument

也可以使用其他包管理器:

pnpm add @cieljs/instrument
bun add @cieljs/instrument
npm install @cieljs/instrument

Quick Start

创建一个 Interceptor:

import { createInstrumenter, type Interceptor } from '@cieljs/instrument';

const loggerInterceptor: Interceptor = {
  intercept(_target, context) {
    if (!context) {
      return;
    }

    return next =>
      function (...args) {
        console.log(`[${context.name}] start`);

        try {
          return Reflect.apply(next, this, args);
        } finally {
          console.log(`[${context.name}] end`);
        }
      };
  },
};

创建 Instrumenter:

const instrument = createInstrumenter(loggerInterceptor);

然后包装任意函数:

const add = instrument(
  (a: number, b: number) => {
    return a + b;
  },
  {
    name: 'math.add',
  },
);

add(1, 2);

输出:

[math.add] start
[math.add] end

函数签名不会发生变化:

// (a: number, b: number) => number
add;

Instrument Context

每一个被观察的操作都可以拥有自己的 Context。

interface InstrumentContext<TMetadata> {
  readonly name: string;
  readonly metadata?: Readonly<Partial<TMetadata>>;
}

其中:

name

稳定的操作标识。

{
  name: 'ciel.memory.search',
}

建议使用稳定且具有层级关系的名称:

ciel.memory.search
ciel.memory.write
ciel.signal.emit
ciel.percept.record

它描述的是:

这个操作是什么。

而不是某一次调用发生了什么。

metadata

与操作本身相关的静态信息。

{
  name: 'ciel.memory.search',

  metadata: {
    pluginId: 'memory',
    capability: 'memory',
  },
}

Metadata 类型可以通过 createInstrumenter 定义:

interface Metadata {
  pluginId: string;
  capability: string;
  query: string;
}

const instrument = createInstrumenter<Metadata>(interceptor);

Derived Instrument

很多情况下,一组函数属于同一个模块、插件或者 Capability。

你不需要为每一个函数重复声明相同的 Context。

可以使用 .with() 派生新的 Instrument:

const instrument = createInstrumenter<Metadata>(interceptor);

const memoryInstrument = instrument.with({
  metadata: {
    pluginId: 'memory',
  },
});

const searchInstrument = memoryInstrument.with({
  name: 'ciel.memory.search',

  metadata: {
    capability: 'memory',
  },
});

然后:

const searchMemory = searchInstrument(
  async query => {
    return memory.search(query);
  },
  {
    metadata: {
      query: 'hello',
    },
  },
);

最终 Interceptor 得到的 Context:

{
  name: 'ciel.memory.search',

  metadata: {
    pluginId: 'memory',
    capability: 'memory',
    query: 'hello',
  },
}

这样可以自然形成:

Instrumenter
    │
    └── Plugin Instrument
            │
            └── Operation Instrument
                    │
                    └── Call-site Context

Context Ownership

.with() 不只是简单地合并对象。

@cieljs/instrument 将越早声明的 Preset 视为越接近 Instrument Owner 的信息。

因此:

上层 Preset
     ↓
下层 Preset
     ↓
调用 Context

优先级为:

上层 Preset > 下层 Preset > 调用 Context

例如:

const pluginInstrument = instrument.with({
  metadata: {
    pluginId: 'trusted-plugin',
  },
});

const operationInstrument = pluginInstrument.with({
  name: 'ciel.memory.search',
});

调用方即使提供:

operationInstrument(fn, {
  name: 'forged.operation',

  metadata: {
    pluginId: 'forged-plugin',
  },
});

最终仍然得到:

{
  name: 'ciel.memory.search',

  metadata: {
    pluginId: 'trusted-plugin',
  },
}

这意味着:

Owner 声明身份,调用方补充信息。

调用方无法覆盖已经由上层 Instrument 固定的字段。

Interceptor

Interceptor 决定:

这个函数是否需要被我观察,以及我要如何包装它。

interface Interceptor<TMetadata> {
  intercept<T extends AnyFunction>(
    target: T,
    context?: InstrumentContext<TMetadata>,
  ): InterceptorWrapper<T> | undefined;
}

如果不希望处理目标:

const interceptor: Interceptor = {
  intercept(target) {
    if (target.name !== 'observed') {
      return undefined;
    }

    return next => {
      // ...
    };
  },
};

因此一个 Instrumenter 可以同时安装不同用途的 Interceptor:

const instrument = createInstrumenter(
  telemetryInterceptor,
  loggerInterceptor,
  devtoolsInterceptor,
  profilingInterceptor,
);

每一个 Interceptor 都可以拥有自己的匹配逻辑。

Wrapper Composition

Interceptor 返回一个 Wrapper:

type InterceptorWrapper<T> = (next: T) => T;

例如:

const first: Interceptor = {
  intercept() {
    return next =>
      function (...args) {
        console.log('first:before');

        const result = Reflect.apply(next, this, args);

        console.log('first:after');

        return result;
      };
  },
};

多个 Wrapper 按照 Interceptor 的声明顺序嵌套:

createInstrumenter(first, second, third);

执行顺序:

first:before
  second:before
    third:before
      target
    third:after
  second:after
first:after

这与常见的 middleware / around advice 模型保持一致。

Preserve Function Semantics

Instrumentation 不应该改变目标函数本身的调用语义。

this

原始 receiver 会继续传递给目标函数:

const calculate = instrument(function (this: { base: number }, delta: number) {
  return this.base + delta;
});

calculate.call(
  {
    base: 2,
  },
  3,
);

结果仍然是:

5

Promise

异步函数保持原始 Promise:

const load = instrument(async () => {
  return 42;
});

await load();

Error

异常不会被 Instrument 自动吞掉:

const execute = instrument(() => {
  throw new Error('failed');
});

execute();

除非某个 Interceptor 明确改变这一行为,否则异常仍会按照原函数语义向上传播。

Why Instrument?

@cieljs/instrument 并不是 Logger。

也不是 Tracer。

更不是完整的 Observability SDK。

它只负责一件事情:

在不侵入业务实现的情况下,为普通函数建立统一、类型安全、可组合的观测入口。

因此具体能力可以作为 Interceptor 构建在它之上:

                  ┌── Logger
                  │
                  ├── OpenTelemetry
                  │
Function ─ Instrument ── DevTools
                  │
                  ├── Metrics
                  │
                  ├── Profiler
                  │
                  └── Custom Interceptor

Instrument 不关心最终数据被发送到哪里。

它只提供稳定的 instrumentation boundary。

Example: Telemetry

例如可以实现一个简单的 Telemetry Interceptor:

const telemetryInterceptor: Interceptor = {
  intercept(_target, context) {
    if (!context) {
      return;
    }

    return next =>
      function (...args) {
        const startedAt = performance.now();

        try {
          const result = Reflect.apply(next, this, args);

          return result;
        } finally {
          telemetry.record({
            name: context.name,
            metadata: context.metadata,
            duration: performance.now() - startedAt,
          });
        }
      };
  },
};

以后同样可以接入:

OpenTelemetry
Sentry
Pino
DevTools
Custom Trace Collector

而业务函数无需知道这些系统的存在。

Design Philosophy

@cieljs/instrument 保持几个简单的设计原则。

Instrumentation should be external.

业务逻辑不应该为了可观测性而改变自己的职责。

Context should describe identity.

Instrument Context 描述操作是什么,而不是承担完整的运行时状态管理。

Owners should own identity.

上层 Instrument 固定的信息不能被不可信的下层调用覆盖。

Interceptors should be composable.

Logging、Tracing、Metrics 和 DevTools 不应该互相依赖。

Functions should stay functions.

不引入新的 Handler、Task、Command 或 Procedure 抽象。

你交给 Instrument 的是普通函数。

最终得到的依然是普通函数。

API

createInstrumenter()

const instrument = createInstrumenter(...interceptors);

创建一个独立的 Instrumenter。

不同 Instrumenter 之间不会共享 Interceptor 或状态。

instrument()

const wrapped = instrument(target, context);

为函数应用所有匹配的 Interceptor。

instrument.with()

const derived = instrument.with({
  name: 'ciel.memory.search',

  metadata: {
    pluginId: 'memory',
  },
});

创建一个继承当前 Preset 的新 Instrument。

原 Instrument 不会被修改。

Interceptor

const interceptor: Interceptor = {
  intercept(target, context) {
    return next => next;
  },
};

定义一个函数拦截器。

返回 undefined 表示跳过目标。

License

MIT