Pyroscope Utility Library
Description
@pawells/pyroscope provides continuous profiling instrumentation for Node.js services via Pyroscope. It wraps @pyroscope/nodejs with:
- PyroscopeExporter — implements
MetricsExporter(Name = 'pyroscope') and self-registers with the@pawells/metricsregistry duringPyroscopeManager.Start(). Its consumer callbacks (OnDescriptorRegistered/OnMetricRecorded) are intentional no-ops — Pyroscope ingests profiles through its own agent, not foreign metric values.Flush()surfaces the latest aggregate counters to every registered backend;Shutdown()stops the profiler. - PyroscopeManager — static lifecycle management (
Start,Shutdown,TrackFunction,CollectAndRecord) for profiling operations - Profiling decorators — class-level (
@Profile) and method-level (@ProfileMethod,@ProfileAsync) decorators for automatic instrumentation - Health-state management —
Healthgetter andPyroscopeHealthStatesenum (healthy/degraded/unhealthy) - Aggregate metrics — CPU samples, memory allocations, HTTP request counters, and response time tracking recorded via
RecordCPUSample,RecordMemorySample, andRecordRequest. These counters are automatically published into the@pawells/metricsregistry fan-out on a configurable periodic timer, surfacing in Prometheus, OpenTelemetry, and any other registered backend.
All profiling data is tagged with global metadata (application name, version, environment) set during initialization. Tags are tracked for metrics aggregation; note that per-profile dynamic tagging is not supported by the underlying @pyroscope/nodejs SDK.
Requirements
- Node.js ≥ 22.0.0
- TypeScript ~6.0.0
- Dependency
@pawells/metrics(foundation library for descriptor-first metric registration) - Dependency
@pyroscope/nodejs~0.4.13 (Pyroscope SDK)
See package.json for the full dependency list and versions.
Installation
Install the package and its dependencies:
yarn add @pawells/pyroscope
Quick Start
1. Initialize the Manager
import { PyroscopeManager } from '@pawells/pyroscope';
// Start profiling at application startup
await PyroscopeManager.Start();
// Shutdown gracefully at application exit
process.on('SIGTERM', async () => {
await PyroscopeManager.Shutdown();
process.exit(0);
});
2. Profile a Function
// Wrap any sync or async function with automatic profiling
const result = await PyroscopeManager.TrackFunction('expensiveQuery', async () => {
return await database.query('SELECT * FROM users');
}, { userId: '123' });
3. Use Method Decorators
import { ProfileMethod, ProfileAsync } from '@pawells/pyroscope';
class UserService {
// Decorator for both sync and async methods
@ProfileMethod({ tags: { operation: 'fetch' } })
async getUser(id: string) {
return await this.database.findById(id);
}
// Explicit async decorator
@ProfileAsync({ name: 'expensive.operation', tags: { service: 'api' } })
async expensiveAsyncOperation() {
// Profiling is automatically started and stopped
return await someHeavyWork();
}
}
4. Profile an Entire Class
import { Profile } from '@pawells/pyroscope';
@Profile({ tags: { service: 'auth' } })
export class AuthService {
// All methods are automatically profiled
async validateToken(token: string) {
// Profiled as 'AuthService.validateToken'
return await this.tokenValidator.check(token);
}
createToken(payload: object) {
// Profiled as 'AuthService.createToken'
return this.tokenGenerator.sign(payload);
}
}
5. Record Metrics Manually
// Record CPU profiling sample (e.g. from a custom profiler)
PyroscopeManager.RecordCPUSample(45); // milliseconds
// Record memory allocation
PyroscopeManager.RecordMemorySample(1024 * 1024); // bytes (1 MB)
// Record HTTP request
PyroscopeManager.RecordRequest(200, 125); // status code, duration in ms
6. Check Health Status
const health = PyroscopeManager.Health;
console.log(`Status: ${health.status}`);
console.log(`Initialized: ${health.initialized}`);
console.log(`Active profiles: ${health.activeProfiles}`);
console.log(`Total recorded: ${health.totalProfiles}`);
Configuration
Configuration is loaded from environment variables with the PYROSCOPE_ prefix using @pawells/config v3. Access via PyroscopeConfig.Get(key).
| Variable | Type | Default | Description |
|---|---|---|---|
PYROSCOPE_ENABLED |
boolean | false |
Globally enable or disable Pyroscope profiling |
PYROSCOPE_APPLICATION_NAME |
string | 'nestjs-app' |
Application name label sent to Pyroscope |
PYROSCOPE_APPLICATION_VERSION |
semver | optional | Application version label sent to Pyroscope (e.g. 1.0.0) |
PYROSCOPE_SERVER_ADDRESS |
URL | 'http://localhost:4040' |
URL of the Pyroscope server. HTTPS is required in all environments except development (see PYROSCOPE_ENVIRONMENT); a plaintext HTTP address causes Start() to throw PyroscopeError with code PYROSCOPE_INSECURE_URL. |
PYROSCOPE_AUTH_USERNAME |
string | optional | Basic auth username for Pyroscope server |
PYROSCOPE_AUTH_PASSWORD |
string | optional | Basic auth password for Pyroscope server |
PYROSCOPE_ENVIRONMENT |
string | 'production' |
Environment label (e.g. production, staging, development) |
PYROSCOPE_PROFILING_MAX_METRICS_HISTORY |
number | 1000 |
Maximum number of profiling metrics to keep in memory |
PYROSCOPE_PROFILING_MAX_ACTIVE_PROFILES |
number | 10000 |
Maximum number of active (in-progress) profiling sessions before new sessions are rejected |
PYROSCOPE_PROFILING_STALE_PROFILE_TIMEOUT_MS |
number | 1800000 |
Time in milliseconds after which a stale profile is considered expired (30 minutes) |
PYROSCOPE_PROFILING_TAGS |
JSON string | '{}' |
Global tags to include with all profiling data as JSON string (e.g. '{"env":"prod"}') |
PYROSCOPE_PROFILING_DEGRADED_ACTIVE_PROFILES_THRESHOLD |
number | 1000 |
Active-profile count above which the health indicator returns degraded status |
PYROSCOPE_PROFILING_METRICS_INTERVAL_MS |
number | 15000 |
Interval (ms) at which aggregate profiling counters are collected and recorded into the @pawells/metrics registry |
Example: Basic .env
PYROSCOPE_ENABLED=true
PYROSCOPE_APPLICATION_NAME=my-service
PYROSCOPE_APPLICATION_VERSION=1.2.3
PYROSCOPE_SERVER_ADDRESS=http://pyroscope-server:4040
PYROSCOPE_ENVIRONMENT=production
PYROSCOPE_PROFILING_TAGS={"service":"api","region":"us-east-1"}
Example: With Basic Auth
PYROSCOPE_ENABLED=true
PYROSCOPE_SERVER_ADDRESS=http://pyroscope-server:4040
PYROSCOPE_AUTH_USERNAME=user
PYROSCOPE_AUTH_PASSWORD=pass
API Reference
PyroscopeManager
Static class controlling the profiling lifecycle and recording metrics.
Methods
| Method | Signature | Description |
|---|---|---|
Start |
async Start(): Promise<void> |
Initializes and starts the Pyroscope profiler. Configures the client with server address, app name, and auth credentials. Logs a warning if unsupported config keys are set. |
Shutdown |
async Shutdown(): Promise<void> |
Stops the profiler and cleans up resources. Clears all active and completed profiling sessions. |
StartProfiling |
StartProfiling(context: TPyroscopeProfileContext): string |
Begins profiling a named operation. Generates a unique profile ID, tracks the context, and evicts stale profiles if active limit exceeded. Returns the generated profile ID. |
StopProfiling |
StopProfiling(context: TPyroscopeProfileContext): TPyroscopeProfileMetrics |
Ends profiling for an operation. Calculates duration, records metrics, and cleans up context. Returns the recorded metrics. |
TrackFunction |
async TrackFunction<T>(name: string, fn: () => T | Promise<T>, tags?: Record<string, string>): Promise<T> |
Wraps a function call with automatic profiling start and stop. Handles both sync and async functions. |
RecordCPUSample |
RecordCPUSample(duration: number): void |
Records a CPU profiling sample. Increments sample counter and adds duration to total CPU time. Negative durations are ignored. Manual-only API. |
RecordMemorySample |
RecordMemorySample(bytes: number): void |
Records a memory profiling sample. Increments sample counter and adds allocation size to total. Negative bytes are ignored. Manual-only API. |
RecordRequest |
RecordRequest(statusCode: number, duration: number): void |
Records an HTTP request with status code and duration. Updates success/failure counters (2xx-3xx = success). Negative durations are ignored. |
ResetProfilingMetrics |
ResetProfilingMetrics(): void |
Resets all aggregate profiling metric counters to zero. Active profiling sessions are not affected. Also resets the delta baseline so the next CollectAndRecord call starts from zero. |
CollectAndRecord |
CollectAndRecord(): void |
Collects the current aggregate profiling counters and records them as deltas into the @pawells/metrics registry. Monotonic counters are recorded as the increment since the previous call; the active-sessions gauge (pyroscope_active_profiles) is always absolute. Called automatically on the periodic timer and by PyroscopeExporter.Flush(); may also be called manually. Throws PyroscopeError if a downstream exporter fails. |
Getters
| Getter | Return Type | Description |
|---|---|---|
Initialized |
boolean |
Whether the profiler has been successfully initialized via Start(). |
State |
PyroscopeHealthStates |
Current health state enum: Healthy, Degraded, or Unhealthy. |
Health |
TPyroscopeManagerHealth |
Complete health status object with status, enabled, initialized, serverAddress, applicationName, activeProfiles, totalProfiles, lastProfileAt. |
ActiveProfiles |
ReadonlyMap<string, TPyroscopeProfileContext> |
Live read-only view of all currently active (in-flight) profiling sessions keyed by profile ID. Changes to the map are reflected immediately. |
Metrics |
readonly TPyroscopeProfileMetrics[] |
Live read-only view of all recorded profiling metrics from completed sessions. Changes to the array are reflected immediately. |
ProfilingMetrics |
TPyroscopeProfilingMetrics |
Aggregate profiling metrics: CPU_SAMPLES, CPU_TOTAL_DURATION, MEMORY_SAMPLES, MEMORY_ALLOCATIONS, REQUESTS_TOTAL, REQUESTS_SUCCESS, REQUESTS_FAILURE, RESPONSE_TIME. |
LastMetricUpdate |
number |
Unix timestamp (ms) of the most recent profiling metric, or 0 if no metrics exist. |
PyroscopeConfig
Static configuration accessor that loads Pyroscope settings from environment variables with the PYROSCOPE_ prefix using @pawells/config. Access configuration values via the static Get(key) method.
import { PyroscopeConfig } from '@pawells/pyroscope';
const enabled = PyroscopeConfig.Get('ENABLED'); // boolean
const serverAddress = PyroscopeConfig.Get('SERVER_ADDRESS'); // string
const appName = PyroscopeConfig.Get('APPLICATION_NAME'); // string
See the Configuration section for a complete list of supported keys.
PyroscopeExporter
Registry-citizen exporter that wires @pawells/pyroscope into the @pawells/metrics fan-out as a first-class MetricsExporter. PyroscopeManager.Start() self-registers an instance; PyroscopeManager.Shutdown() self-deregisters it, so Instrumentation.FlushAll() and Instrumentation.ShutdownAll() work without any additional wiring.
Pyroscope is a producer of its own profiling statistics, not a consumer of arbitrary application metrics. Profiles are streamed to the Pyroscope server by its own agent. For this reason, the consumer callbacks are intentional no-ops:
OnDescriptorRegisteredandOnMetricRecordeddo nothing — this exporter ignores foreign metric descriptors and values.Flush()callsPyroscopeManager.CollectAndRecord(), surfacing the latest aggregate counters to every registered backend.Shutdown()delegates toPyroscopeManager.Shutdown().
Per the exporter error contract, lifecycle hooks propagate failures (throw / reject) rather than swallowing them — the registry isolates and aggregates the failure into a MetricsExporterError.
Property:
| Property | Type | Description |
|---|---|---|
Name |
'pyroscope' |
Stable identifier used by the registry for diagnostics and deduplication. |
Methods:
| Method | Signature | Description |
|---|---|---|
OnDescriptorRegistered |
OnDescriptorRegistered(descriptor: TMetricDescriptor): void |
No-op. Pyroscope does not ingest foreign metric descriptors. |
OnMetricRecorded |
OnMetricRecorded(value: TMetricValue): void |
No-op. Pyroscope does not consume foreign metric values. |
Flush |
async Flush(): Promise<void> |
Calls PyroscopeManager.CollectAndRecord(). Throws PyroscopeError on failure. |
Shutdown |
async Shutdown(): Promise<void> |
Calls PyroscopeManager.Shutdown(). Throws PyroscopeError on failure. |
Example:
import { Instrumentation } from '@pawells/metrics';
// PyroscopeExporter is constructed internally by PyroscopeManager.Start()
// and automatically registered with the Instrumentation registry.
// No manual registration is required.
// During graceful shutdown, call either:
await Instrumentation.ShutdownAll();
// OR
await PyroscopeManager.Shutdown();
Profiling Metrics Emitted
When the profiler is running, @pawells/pyroscope publishes the following metrics into the @pawells/metrics registry on each CollectAndRecord tick. These metrics surface in every registered backend — Prometheus, OpenTelemetry, or any custom MetricsExporter.
Monotonic counters are recorded as deltas since the previous tick so backend counter instruments accumulate correctly without double-counting. pyroscope_active_profiles is always an absolute gauge value.
| Metric | Type | Label | Description |
|---|---|---|---|
pyroscope_cpu_samples_total |
Counter | — | Total CPU profiling samples recorded |
pyroscope_cpu_duration_milliseconds_total |
Counter | — | Total CPU profiling sample duration (ms) |
pyroscope_memory_samples_total |
Counter | — | Total memory profiling samples recorded |
pyroscope_memory_allocated_bytes_total |
Counter | — | Total bytes of memory allocation observed |
pyroscope_requests_total |
Counter | outcome: success|failure |
HTTP requests recorded by the profiler |
pyroscope_response_time_milliseconds_total |
Counter | — | Total request/response duration (ms) |
pyroscope_active_profiles |
Gauge | — | In-flight profiling sessions at collection time |
Registry Constants
PyroscopeMetricNames
Enum of stable string constants for the seven metrics that @pawells/pyroscope registers with the @pawells/metrics registry. Use these constants to build dashboards or alerts without hard-coding metric name strings.
export enum PyroscopeMetricNames {
CpuSamplesTotal = 'pyroscope_cpu_samples_total',
CpuDurationMillisecondsTotal = 'pyroscope_cpu_duration_milliseconds_total',
MemorySamplesTotal = 'pyroscope_memory_samples_total',
MemoryAllocatedBytesTotal = 'pyroscope_memory_allocated_bytes_total',
RequestsTotal = 'pyroscope_requests_total',
ResponseTimeMillisecondsTotal = 'pyroscope_response_time_milliseconds_total',
ActiveProfiles = 'pyroscope_active_profiles'
}
PYROSCOPE_PROFILING_DESCRIPTORS
readonly TMetricDescriptor[] — the full set of metric descriptors that PyroscopeManager.Start() registers with the registry. Useful for pre-registering descriptors in test setups or when building custom instrumentation that needs to reference the same descriptor definitions.
import { Instrumentation } from '@pawells/metrics';
import { PYROSCOPE_PROFILING_DESCRIPTORS } from '@pawells/pyroscope';
for (const descriptor of PYROSCOPE_PROFILING_DESCRIPTORS) {
if (Instrumentation.GetDescriptor(descriptor.name) === undefined) {
Instrumentation.RegisterDescriptor(descriptor);
}
}
PYROSCOPE_REQUEST_OUTCOME_LABEL
string — the label name ('outcome') used to partition pyroscope_requests_total into success and failure series. Use this constant instead of the literal string when writing PromQL queries or OTel attribute filters.
import { PYROSCOPE_REQUEST_OUTCOME_LABEL } from '@pawells/pyroscope';
// 'outcome'
console.log(PYROSCOPE_REQUEST_OUTCOME_LABEL);
Profiling Decorators
@Profile
Class-level decorator that profiles all methods in a class.
@Profile(options?: ProfileClassOptions): ClassDecorator
Options:
| Option | Type | Description |
|---|---|---|
tags |
Record<string, string> |
Global metadata tags to attach to all method profiles within the class. Merged with per-method tags. Tags are tracked for metrics only, not forwarded to the SDK. |
Behavior:
- Automatically wraps every method (except constructor) with profiling start/stop
- Works with both sync and async methods
- Profile name formatted as
ClassName.methodName - Silently skips profiling if
ENABLED=falseor manager not initialized
Example:
@Profile({ tags: { service: 'user' } })
@Injectable()
export class UserService {
async findById(id: string) {
// Profiled as 'UserService.findById'
}
}
@ProfileMethod
Method-level decorator for selective profiling of specific methods.
@ProfileMethod(options?: ProfileMethodOptions): MethodDecorator
Options:
| Option | Type | Description |
|---|---|---|
name |
string |
Custom name for the profiled operation. If not provided, uses ClassName.methodName. |
tags |
Record<string, string> |
Metadata tags to attach to this profile. Tracked for metrics aggregation only. |
Behavior:
- Works with both sync and async methods
- Automatically detects Promises and handles async completion
- Silently skips profiling if
ENABLED=falseor manager not initialized
Example:
class DatabaseService {
@ProfileMethod({ name: 'db.query.expensive' })
async executeExpensiveQuery(sql: string) {
return await this.db.query(sql);
}
}
@ProfileAsync
Method-level decorator specifically designed for async methods.
@ProfileAsync(options?: ProfileMethodOptions): MethodDecorator
Options: Same as @ProfileMethod.
Behavior:
- Designed for async methods (returning Promise)
- Guarantees proper Promise handling and timing accuracy
- Silently skips profiling if
ENABLED=falseor manager not initialized
Example:
class ApiService {
@ProfileAsync({ name: 'api.fetch.user', tags: { endpoint: 'users' } })
async fetchUser(userId: string) {
return await this.http.get(`/users/${userId}`).toPromise();
}
}
Enums
PyroscopeHealthStates
Health status enumeration for profiler state.
export enum PyroscopeHealthStates {
Healthy = 'healthy', // All services operational
Degraded = 'degraded', // Services running with issues (e.g. high active profiles)
Unhealthy = 'unhealthy' // Services not operational
}
PyroscopeProfileTypes
Profile type enumeration. Only Node.js-supported types are included.
export enum PyroscopeProfileTypes {
CPU = 'cpu',
Memory = 'memory'
}
Error Classes
PyroscopeError
Base error class for Pyroscope-related errors. Extends @pawells/typescript-common BaseError with optional context string for diagnostics.
constructor(message: string, options: IPyroscopeErrorOptions = {})
Options:
code: string (default:'PYROSCOPE_ERROR') — Error code for programmatic identificationcontext: string (optional) — Context string providing additional diagnostic detailcause: Error (optional) — Original error that caused this error (cause-chain propagation)
Getters:
Code: string — Error code for programmatic identificationContext: string | undefined — Context string, if set
Example:
try {
await someProfilingOperation();
} catch (error) {
throw new PyroscopeError('Profiling failed', {
code: 'PYROSCOPE_OPERATION_FAILED',
context: 'PyroscopeManager.TrackFunction',
cause: error instanceof Error ? error : undefined
});
}
Types
TPyroscopeProfileContext — Context for an in-flight profiling session.
{
functionName: string; // Operation name
className?: string; // Class name (for method profiles)
methodName?: string; // Method name (for method profiles)
profileId?: string; // Unique ID (set by StartProfiling)
tags?: Record<string, string>; // Metadata tags
startTime?: number; // Start timestamp (ms)
endTime?: number; // End timestamp (ms)
duration?: number; // Elapsed milliseconds
error?: Error; // Error, if occurred
}
TPyroscopeProfileMetrics — Metrics from a completed profiling session.
{
durationMs: number; // Elapsed milliseconds
timestamp: number; // Completion timestamp (ms)
tags?: Record<string, string>; // Metadata tags
}
TPyroscopeProfilingMetrics — Aggregate profiling metrics.
{
CPU_SAMPLES: number; // Count of CPU samples recorded
CPU_TOTAL_DURATION: number; // Total CPU duration (ms)
MEMORY_SAMPLES: number; // Count of memory samples recorded
MEMORY_ALLOCATIONS: number; // Total memory allocated (bytes)
REQUESTS_TOTAL: number; // Total HTTP requests recorded
REQUESTS_SUCCESS: number; // Count of successful requests (2xx-3xx)
REQUESTS_FAILURE: number; // Count of failed requests
RESPONSE_TIME: number; // Total response time (ms)
}
TPyroscopeManagerHealth — Complete health status object returned by PyroscopeManager.Health.
{
status: PyroscopeHealthStates; // Current health state
enabled: boolean; // Whether profiling is enabled
initialized: boolean; // Whether manager is initialized
serverAddress: string; // Configured Pyroscope server URL
applicationName: string; // Configured application name
activeProfiles: number; // Count of in-flight profiling sessions
totalProfiles: number; // Total recorded profiling sessions
lastProfileAt: number; // Timestamp of most recent metric (ms)
}
Security note: The
serverAddressfield is intentionally omitted fromTPyroscopeHealthResponse(the type intended for external-facing health responses a consuming application may choose to expose) to avoid leaking internal infrastructure details.serverAddressis only present on the in-processTPyroscopeManagerHealthobject returned byPyroscopeManager.Health. This package does not itself register any HTTP endpoints — health status must be surfaced by the consuming application.
License
MIT — Phillip Aaron Wells. See LICENSE for details.