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

@scayle/opentelemetry

v0.1.0-alpha.1

Published

SCAYLE OpenTelemetry module for Inertia based storefront applications

Readme

@scayle/opentelemetry

OpenTelemetry integration for Storefront Application V3. Provides a Vite plugin for build-time SDK initialization and a Hono middleware for request instrumentation.

Features

  • HTTP request tracing with OpenTelemetry semantic conventions
  • OTLP trace and metrics export
  • Node auto-instrumentations (HTTP, undici/fetch, runtime metrics)
  • Path filtering and normalization for route names
  • Request/response header capture
  • Works in both dev server and production builds

Installation

The package is included in the V3 template by default. To add it manually:

pnpm add @scayle/opentelemetry

Quick Start

1. Add the Vite plugin

// vite.config.ts
import { defineConfig } from 'vite'
import storefrontBuild from '@scayle/storefront-build'
import { opentelemetryPlugin } from '@scayle/opentelemetry/vite'

export default defineConfig({
  plugins: [
    storefrontBuild({
      serverEntry: './src/server/index.ts',
      ssrEntry: './src/client/ssr.ts',
      indexEntry: './src/client/index.html',
    }),
    opentelemetryPlugin(),
  ],
  envPrefix: ['STOREFRONT_', 'OTEL_'], // Required for OTEL env vars in dev
})

2. Add the Hono middleware

// src/server/index.ts
import { Hono } from 'hono'
import { opentelemetry } from '@scayle/opentelemetry'

const app = new Hono()

// Add early in the middleware chain, after static file serving
app.use(
  opentelemetry({
    pathReplace: ['^/(en|de|fr)/', '/:locale/'],
    requestHeaders: ['x-request-id'],
  }),
)

Configuration

Environment Variables

The SDK reads standard OpenTelemetry environment variables at startup:

| Variable | Description | Example | | ----------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------- | | OTEL_SERVICE_NAME | Sets the service name in traces | storefront-v3 | | OTEL_RESOURCE_ATTRIBUTES | Additional resource attributes | deployment.environment=production,service.version=1.0.0 | | OTEL_EXPORTER_OTLP_ENDPOINT | OTLP collector endpoint | http://localhost:4318 | | OTEL_EXPORTER_OTLP_HEADERS | Headers for OTLP requests | api-key=secret | | OTEL_TRACES_SAMPLER_ARG | Sampling argument | 0.1 | | OTEL_TRACES_EXPORTER | Trace exporter selection. One of otlp (default), console, none. | console | | OTEL_METRICS_EXPORTER | Metric exporter selection. One of otlp (default), console, none. | none |

Unknown values for OTEL_TRACES_EXPORTER and OTEL_METRICS_EXPORTER log a single warn (logger namespace opentelemetry) and fall back to otlp. The matching OTEL_LOGS_EXPORTER env var is intentionally not honored: the storefront bridges OTEL log records into tslog via TslogLogRecordExporter, so log output already reaches the console without an OTLP collector.

See the OpenTelemetry Environment Variable Specification for the complete list of supported variables.

Important: Dev Server Configuration

Vite filters environment variables by prefix. To ensure OTEL variables are available in dev, add OTEL_ to envPrefix in vite.config.ts:

export default defineConfig({
  envPrefix: ['STOREFRONT_', 'OTEL_'],
  // ...
})

In production (running node .output/server/index.mjs directly), all environment variables are available and this is not needed.

Middleware Options

app.use(
  opentelemetry({
    // Regex pattern for paths to ignore (no spans created)
    pathBlocklist: '^/health|/_assets/',

    // Normalize route names (e.g., /de/products -> /:locale/products)
    pathReplace: ['^/(en|de|fr)/', '/:locale/'],

    // Request headers to capture as span attributes
    requestHeaders: ['x-request-id'],

    // Response headers to capture as span attributes
    responseHeaders: ['x-trace-id', 'cache-control'],

    // Custom filter function (return true to skip)
    ignoreRequestHook: (c) => c.req.path.startsWith('/_nuxt'),
  }),
)

Vite Plugin Options

opentelemetryPlugin({
  // Enable/disable the plugin (default: true)
  enabled: true,

  // Module specifiers to include in import-in-the-middle hooks
  include: ['@scayle/*'],

  // Module specifiers to exclude
  exclude: ['node_modules'],

  // Skip auto-instrumentation for matching request paths
  // (passed to @opentelemetry/instrumentation-http and -undici).
  // Must be self-contained — serialized via .toString() into the prod entry.
  filterPath: (path) => path === '/api/up',
})

Health probe filtering

The defaults suppress traces for the /api/up health probe at both layers:

// vite.config.ts — auto-instrumentation (the only layer that fires for /api/up
// in production, since the route is registered before global middleware in the
// boilerplate)
import { defaultPathFilter } from '@scayle/opentelemetry'
import { opentelemetryPlugin } from '@scayle/opentelemetry/vite'

opentelemetryPlugin({ filterPath: defaultPathFilter })

// src/server/index.ts — Hono middleware (defense-in-depth, covers tenants
// who reorder routes so /api/up flows through global middleware)
import {
  defaultOpenTelemetryConfig,
  opentelemetry,
} from '@scayle/opentelemetry'

app.use(opentelemetry(defaultOpenTelemetryConfig))

To filter additional paths (e.g. a custom /api/ready probe), replace defaultPathFilter with your own self-contained function and extend pathBlocklist:

opentelemetryPlugin({
  filterPath: (path) => path === '/api/up' || path === '/api/ready',
})

app.use(
  opentelemetry({
    ...defaultOpenTelemetryConfig,
    pathBlocklist: '^/api/(up|ready)$',
  }),
)

How It Works

Production Build

The Vite plugin wraps the server entry to:

  1. Register import-in-the-middle hooks before any app modules load
  2. Initialize the NodeSDK with OTLP exporters and auto-instrumentations
  3. Dynamically import the original server entry

This ensures all modules (including dependencies) get properly instrumented.

Dev Server

The plugin uses Vite's configureServer hook to:

  1. Register import-in-the-middle hooks when the dev server starts
  2. Initialize the NodeSDK with a shared tracer provider
  3. The Hono middleware (loaded via ssrLoadModule) picks up the initialized tracer

Note: Some auto-instrumentations (like http) may have limited coverage in dev if Node's built-in modules are loaded before configureServer runs. The Hono middleware spans always work.

Architecture

Request → Hono Middleware → Span created → Next handler
                              ↓
                         Response → Span attributes set → Span exported

The middleware creates a child span under the active OpenTelemetry context for each HTTP request. It captures:

  • Method, path, scheme, status code
  • Route pattern (via c.req.routePath)
  • Client address, user agent
  • Query string
  • Configured request/response headers
  • Errors (5xx responses mark span as ERROR)

Auto-instrumentation policy

initSDK() registers an explicit, curated set of OpenTelemetry instrumentations matched to the V3 reference stack. It does NOT use @opentelemetry/auto-instrumentations-node. The metapackage bundles 40+ patchers. With 32 of them irrelevant to V3 (Hono, undici fetch, node-redis, tslog), it paid measurable boot and per-module-load cost (visible as elevated makeSyncRequest activity in the import-in-the-middle hook) for no observability benefit. An explicit list also fails loudly when upstream adds a new patcher we did not opt into.

The kept set:

| Instrumentation | Why kept | | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | @opentelemetry/instrumentation-http | Inbound HTTP entry span. Backbone of the route view in trace UIs. | | @opentelemetry/instrumentation-undici | Outbound fetch client (every SAPI call from the storefront). | | @opentelemetry/instrumentation-dns | DNS lookup spans. Cheap, surfaces resolver outliers. | | @opentelemetry/instrumentation-redis | node-redis command spans. 0.66.0 supports redis >=2.6.0 <6 through its v4-v5 patcher, so the scayle-kv driver's redis 5.12.1 client is covered. Client-level metrics are a separate signal, emitted natively by node-redis (see Native node-redis metrics). | | @opentelemetry/instrumentation-runtime-node | Node runtime metrics (heap, event loop, GC). Used by V3 runtime dashboards. |

Plus @orpc/otel's ORPCInstrumentation for the API router.

Tenants that diverge from this stack (e.g. add the AWS SDK for S3 / SQS calls) opt back in via additionalInstrumentations (next section).

Native node-redis metrics

Separate from the redis spans above, initSDK() also turns on node-redis's own OpenTelemetry metrics, which shipped in redis 5.12.0 (OpenTelemetry.init() from the redis package). After sdk.start(), the SDK calls that bootstrap so the scayle-kv driver's client emits client-level metrics through the same metric pipeline the SDK already configures (OTLP by default, or whatever OTEL_METRICS_EXPORTER selects). Spans and metrics are different signals; both run.

The bootstrap is best-effort: it uses a guarded dynamic import('redis'), so a missing redis (it is an optional peer dependency) or a double OpenTelemetry.init() is logged via diag and never blocks startup.

Enabled metric groups

node-redis groups its metrics. The SDK enables the four groups relevant to the kv driver in a clustered deployment: command, connection-basic, connection-advanced, and resiliency. The cluster-specific signals (connection handoff on slot migration, relaxed timeouts during maintenance) live in connection-basic. connection-advanced adds per-pool saturation metrics, which matter because the cluster client keeps one connection pool per node.

| Metric | Instrument | Unit | Group | Description | | ----------------------------------------- | ------------- | ---------------- | --------------------- | -------------------------------------------------------------------------------- | | db.client.operation.duration | Histogram | s | command | Duration of a client operation (includes retries). | | db.client.connection.count | UpDownCounter | {connection} | connection-basic | Current number of active connections. | | db.client.connection.create_time | Histogram | s | connection-basic | Time taken to open a new connection. | | redis.client.connection.handoff | Counter | {handoff} | connection-basic | Connections handed off to another node (e.g. after a MOVING / slot migration). | | redis.client.connection.relaxed_timeout | UpDownCounter | {relaxation} | connection-basic | Timeout relaxations applied after a server maintenance notification. | | db.client.connection.wait_time | Histogram | s | connection-advanced | Time spent waiting for an available connection from the pool. | | redis.client.connection.closed | Counter | {connection} | connection-advanced | Total number of closed connections (carries a close-reason attribute). | | redis.client.errors | Counter | {error} | resiliency | All errors, both returned to the caller and handled internally. | | redis.client.maintenance.notifications | Counter | {notification} | resiliency | Maintenance notifications received from the server. |

Each data point carries db.system.name=redis, db.namespace (DB index), server.address, server.port, db.client.connection.pool.name, and redis.client.library (e.g. node-redis:5.12.1).

connection-advanced also declares db.client.connection.pending_requests, but node-redis 5.12.1 ships it without a populating callback, so it currently emits no data points.

Groups left off

pubsub (redis.client.pubsub.messages), streaming (redis.client.stream.lag), and client-side-caching (redis.client.csc.*) are not enabled, because the scayle-kv driver does not use those features. The enabled set is fixed in initSDK.

Viewing metrics locally

Set OTEL_METRICS_EXPORTER=console to print metrics to stdout without a collector. Unlike spans (which batch and flush within a few seconds), the metric reader flushes on its periodic interval (the SDK default, ~60s), so allow up to a minute before the first metric appears, or until the process shuts down gracefully.

initSDK(filterPath?, options?)

Starts the OpenTelemetry NodeSDK with OTLP exporters and the curated instrumentation set.

Parameters

  • filterPath?: (path: string) => boolean: paths returning true are skipped by instrumentation-http and instrumentation-undici. Use for static assets and health probes.
  • options?: InitSDKOptions
    • additionalInstrumentations?: Instrumentation[]: extra instrumentations appended to the curated set.

Example: tenant using the AWS SDK

import { initSDK } from '@scayle/opentelemetry/sdk-init'
import { defaultPathFilter } from '@scayle/opentelemetry'
import { AwsInstrumentation } from '@opentelemetry/instrumentation-aws-sdk'

initSDK(defaultPathFilter, {
  additionalInstrumentations: [new AwsInstrumentation()],
})

Span helpers

traceSsrRender(render)

Wraps an SSR render callback in a vue_ssr_render span so Vue render time and output size appear as a discrete child of inertia_render in the observability platform, separate from Inertia composition and prop work.

The helper lives in this package (rather than @scayle/storefront) so the storefront SDK does not gain a vue dependency. The span shape stays SDK-controlled. Future renames or attribute changes do not touch tenant forks.

Attributes

  • vue.html_bytes: Buffer.byteLength on the rendered string. No extra work, the value is already a string at this point.

Example

import { renderToString } from '@vue/server-renderer'
import { traceSsrRender } from '@scayle/opentelemetry'

createInertiaApp({
  render: (app) => traceSsrRender(() => renderToString(app)),
  // ...
})

Errors

The wrapped callback's rejection is recorded on the span via recordSpanError and rethrown. The helper never swallows errors.

Existing helpers

  • opentelemetry(...): Hono request middleware. Produces the entry server span and propagates the route name to the parent HTTP span.
  • defaultPathFilter: pre-built filter that skips /api/up.
  • defaultOpenTelemetryConfig: pre-built middleware config with V3-standard headers.
  • recordSpanError(span, err, logger): marks a span as ERROR, records the exception, and emits a structured log entry.

Undici hook attributes

initSDK() configures instrumentation-undici with requestHook and responseHook to surface payload sizes and content-encoding directly on every client span. All attributes read from existing wire headers. No body decoding.

| Attribute | Source | When populated | | --------------------------------------- | ---------------------------------- | ------------------------------------------------------- | | http.request.body.size | content-length request header | When present (POST/PUT/PATCH with explicit length). | | http.response.body.size | content-length response header | When the upstream sends one. Chunked transfer omits it. | | http.response.header.content_encoding | content-encoding response header | When the upstream compresses the body (gzip, br). |

These attributes surface unexpectedly large SAPI payloads (e.g. broad with=siblings,siblings.images includes on the listing endpoint) directly in the trace UI without decoding span payloads.

Troubleshooting

Service shows as unknown_service:node

Set OTEL_SERVICE_NAME environment variable. Remember to add OTEL_ prefix to Vite's envPrefix in dev.

No spans exported

Check OTEL_EXPORTER_OTLP_ENDPOINT is set correctly. The SDK defaults to http://localhost:4318 for OTLP/HTTP.

Want to see spans locally without an OTLP collector

Set OTEL_TRACES_EXPORTER=console and the SDK will print each span as JSON on stdout. Combine with OTEL_METRICS_EXPORTER=console for metrics, or OTEL_TRACES_EXPORTER=none / OTEL_METRICS_EXPORTER=none to disable the respective signal entirely. Note that metrics flush on the reader's periodic interval (~60s by default), so they appear later than spans, not immediately. See Native node-redis metrics for what the redis client emits.

Static assets are being traced

Add a pathBlocklist pattern to filter them: pathBlocklist: '^/_nuxt|^/assets/'

Out of Scope

  • Browser/client-side tracing (server-side only)
  • Vercel preset support (Node server only)
  • OpenTelemetry Logs API (using span events for now)