@onurgvnc/tx2
v1.0.1
Published
tx2
Readme
TX2
original project: https://github.com/pm2/tx2 | https://npmjs.com/package/tx2
📋 Table of Contents
- Introduction
- Installation
- Core Concepts
- Features
- API Reference
- Advanced Usage
- Technical Details
- Troubleshooting
- Performance Considerations
Introduction
TX2 is a comprehensive monitoring library that bridges your Node.js applications with the PM2 process manager. It provides real-time metrics, event tracking, remote actions, and error reporting capabilities through PM2's IPC (Inter-Process Communication) mechanism.
What Does TX2 Do?
- 📊 Metrics Collection: Collects and reports CPU, memory, request rate, and custom metrics
- 📬 Event Notifications: Sends important application events to PM2 dashboard
- ⚡ Action Handling: Receives and executes commands from PM2 dashboard
- 🐛 Error Reporting: Captures and reports errors with full stack traces
- 📈 Performance Monitoring: Tracks histograms, counters, meters, and custom measurements
Architecture Overview
TX2 operates through a bidirectional communication channel with PM2:
Your Application <--[IPC via process.send/on]--> PM2 Daemon <--[API]--> PM2 Web Dashboard
| | |
TX2 Library PM2 God Daemon Monitoring InterfaceThe communication flow:
- Metrics: Automatically sent every 990ms via
axm:monitormessages - Events: Instantly transmitted via
human:eventmessages - Actions: Registered via
axm:actionand responded viaaxm:reply - Errors: Immediately reported via
process:exceptionmessages
Installation
For TypeScript Projects
# Copy the tx2.ts file to your project
cp tx2.ts your-project/src/
# Install required dependencies
npm install fast-safe-stringify
npm install --save-dev @types/nodeFor JavaScript Projects
# Install the original tx2 package
npm install tx2
# Or use the TypeScript version and compile it
npx tsc tx2.ts --target ES2020 --module commonjsBasic Setup
import tx2 from './tx2'
// Initialize monitoring when application starts
tx2.event('app:ready', {
version: process.env.npm_package_version,
node: process.version,
pid: process.pid
})
// Graceful shutdown handling
process.on('SIGINT', () => {
tx2.event('app:shutdown', { reason: 'SIGINT' })
process.exit(0)
})Core Concepts
Inter-Process Communication (IPC)
TX2 leverages Node.js's built-in IPC capabilities to communicate with PM2:
- Outbound Messages: Uses
process.send()to transmit data to PM2 - Inbound Messages: Listens via
process.on('message')for PM2 commands - Message Queue: No internal buffering - messages are sent immediately
- Connection Detection: Automatically detects if running under PM2 via
process.sendavailability
Message Protocol
TX2 implements PM2's monitoring protocol with 5 distinct message types:
| Type | Direction | Purpose | Frequency |
| ------------------- | --------- | ------------------------ | ---------------------- |
| axm:monitor | App → PM2 | Metric data transmission | Every 990ms |
| human:event | App → PM2 | Event notifications | On-demand |
| axm:action | App → PM2 | Action registration | Once per action |
| axm:reply | App → PM2 | Action response | After action execution |
| process:exception | App → PM2 | Error reporting | On error |
Data Serialization
TX2 handles complex data serialization challenges:
- Circular References: Safely handled via
fast-safe-stringify - Error Objects: Converted to plain objects preserving all properties
- Large Payloads: No size limits, but large data may impact performance
- Special Properties: Handles
__name,_length, and other meta properties
Features
1. Events
Events allow you to track important occurrences in your application lifecycle. They appear in PM2 logs and can trigger alerts.
Basic Event Usage
// Simple event notification
tx2.event('server:started')
// Event with string data
tx2.event('cache:cleared', 'Redis cache has been flushed')
// Event with numeric data
tx2.event('payment:processed', 150.99)
// Event with boolean data
tx2.event('maintenance:mode', true)Structured Event Data
// Event with object payload (recommended approach)
tx2.event('user:registered', {
userId: 'usr_123456',
email: '[email protected]',
plan: 'premium',
referralSource: 'google',
timestamp: Date.now(),
metadata: {
ip: '192.168.1.1',
userAgent: 'Mozilla/5.0...',
country: 'US'
}
})
// Complex nested event structure
tx2.event('order:completed', {
orderId: 'ORD-2024-001',
customer: {
id: 789,
name: 'John Doe',
email: '[email protected]',
loyaltyPoints: 250,
tier: 'gold'
},
items: [
{
sku: 'PROD-1',
name: 'Premium Widget',
quantity: 2,
price: 29.99,
discount: 5.00
},
{
sku: 'PROD-2',
name: 'Standard Gadget',
quantity: 1,
price: 49.99,
discount: 0
}
],
totals: {
subtotal: 109.97,
discount: 10.00,
tax: 9.90,
shipping: 5.00,
total: 114.87
},
payment: {
method: 'credit_card',
last4: '1234',
processor: 'stripe',
transactionId: 'txn_abc123'
},
fulfillment: {
method: 'standard',
estimatedDelivery: '2024-03-15',
warehouse: 'EAST-1'
}
})Event Naming Conventions
// ✅ Good - Clear namespace:action format
tx2.event('database:connected')
tx2.event('api:request:started')
tx2.event('cache:miss')
tx2.event('auth:login:success')
tx2.event('payment:subscription:renewed')
// ❌ Bad - Ambiguous or unclear names
tx2.event('event1')
tx2.event('something_happened')
tx2.event('data')
tx2.event('update')Event Patterns for Different Scenarios
// Lifecycle Events
tx2.event('app:initializing', { stage: 'database' })
tx2.event('app:ready', { startupTime: Date.now() - startTime })
tx2.event('app:shutdown', { reason: 'SIGTERM', cleanup: true })
// Performance Events
tx2.event('performance:slow_query', {
query: 'SELECT * FROM users...',
duration: 5234,
threshold: 1000
})
// Security Events
tx2.event('security:login:failed', {
username: 'admin',
ip: req.ip,
attempts: failedAttempts,
blocked: failedAttempts > 5
})
// Business Events
tx2.event('business:revenue:milestone', {
type: 'daily',
amount: 10000,
currency: 'USD',
transactions: 156
})2. Actions
Actions enable remote control of your application through the PM2 dashboard. They're perfect for administrative tasks, debugging, and operational controls.
Simple Actions
// Basic action without parameters
tx2.action('clear_cache', (reply) => {
cache.flush()
reply({
success: true,
message: 'Cache cleared successfully',
timestamp: new Date().toISOString()
})
})
// Action with error handling
tx2.action('reload_config', (reply) => {
try {
const oldConfig = { ...config }
config.reload()
reply({
success: true,
changes: diffConfigs(oldConfig, config),
version: config.version
})
} catch (error) {
reply({
success: false,
error: error.message,
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
})
}
})Parameterized Actions
// Action with typed parameters
interface DatabaseBackupOptions {
format: 'sql' | 'json' | 'csv'
compress: boolean
tables?: string[]
}
tx2.action('backup_database',
{ format: 'sql', compress: true },
async (options: DatabaseBackupOptions, reply) => {
try {
const backup = await db.backup({
format: options.format,
compress: options.compress,
tables: options.tables || 'all'
})
reply({
success: true,
filename: backup.filename,
size: backup.size,
tables: backup.tables.length,
compressed: options.compress,
location: backup.path
})
} catch (error) {
reply({
success: false,
error: error.message,
code: error.code
})
}
}
)Asynchronous Actions
// Long-running async action
tx2.action('generate_report', async (reply) => {
const reportId = uuid()
// Immediate acknowledgment
reply({
success: true,
reportId,
status: 'processing',
message: 'Report generation started'
})
// Continue processing in background
generateReportAsync(reportId).catch(error => {
tx2.issue({
type: 'report_generation_failed',
reportId,
error: error.message
})
})
})
// Action with progress updates
tx2.action('migrate_data', async (reply) => {
const stages = ['backup', 'transform', 'validate', 'apply', 'verify']
const results = {}
for (const stage of stages) {
try {
tx2.event('migration:stage', { stage, status: 'started' })
results[stage] = await runMigrationStage(stage)
tx2.event('migration:stage', { stage, status: 'completed' })
} catch (error) {
reply({
success: false,
failedStage: stage,
completedStages: Object.keys(results),
error: error.message
})
return
}
}
reply({
success: true,
stages: results,
duration: Date.now() - startTime
})
})Common Action Patterns
// 1. Toggle Pattern
let maintenanceMode = false
tx2.action('toggle_maintenance', (reply) => {
maintenanceMode = !maintenanceMode
if (maintenanceMode) {
app.disable('x-powered-by')
app.use((req, res) => res.status(503).send('Maintenance in progress'))
} else {
app.enable('x-powered-by')
}
reply({
enabled: maintenanceMode,
message: `Maintenance mode ${maintenanceMode ? 'enabled' : 'disabled'}`,
affectedServices: ['api', 'websocket']
})
})
// 2. Health Check Pattern
tx2.action('health_check', async (reply) => {
const checks = await Promise.allSettled([
db.ping(),
redis.ping(),
elasticsearch.ping(),
messageQueue.health()
])
reply({
healthy: checks.every(c => c.status === 'fulfilled'),
timestamp: new Date().toISOString(),
services: {
database: checks[0].status === 'fulfilled',
redis: checks[1].status === 'fulfilled',
elasticsearch: checks[2].status === 'fulfilled',
queue: checks[3].status === 'fulfilled'
},
system: {
memory: process.memoryUsage(),
uptime: process.uptime(),
cpu: process.cpuUsage()
}
})
})
// 3. Diagnostic Pattern
tx2.action('diagnose', (reply) => {
const diagnostics = {
environment: {
node: process.version,
platform: process.platform,
arch: process.arch,
pid: process.pid,
ppid: process.ppid
},
memory: {
...process.memoryUsage(),
limit: v8.getHeapStatistics().heap_size_limit
},
connections: {
http: server.connections,
websocket: wss.clients.size,
database: db.pool.totalCount,
redis: redis.client.connections
},
queues: {
pending: queue.pending(),
active: queue.active(),
completed: queue.completed(),
failed: queue.failed()
}
}
reply(diagnostics)
})
// 4. Reset Pattern
tx2.action('reset_all_counters', (reply) => {
const counters = [
'requests',
'errors',
'cache_hits',
'cache_misses'
]
counters.forEach(name => {
if (tx2.metricExists(name)) {
tx2.counter(name).reset()
}
})
reply({
reset: true,
counters,
timestamp: Date.now()
})
})3. Metrics
Metrics provide continuous monitoring of your application's performance. TX2 supports multiple metric types, each optimized for different measurement scenarios.
3.1. Simple Metric
Tracks a single value or function result. Ideal for gauges and instantaneous measurements.
// Static value metric
const dbConnections = tx2.metric({
name: 'db_connections',
value: 5,
unit: 'connections'
})
// Update the value
dbConnections.set(10)
// Dynamic value with function
tx2.metric({
name: 'memory_usage',
unit: 'MB',
value: () => {
const used = process.memoryUsage().heapUsed
return Math.round(used / 1024 / 1024)
}
})
// Complex computed metric
let cache = new Map()
tx2.metric({
name: 'cache_efficiency',
unit: '%',
value: () => {
if (cacheRequests === 0) return 0
return Math.round((cacheHits / cacheRequests) * 100)
}
})
// Boolean state metric
tx2.metric({
name: 'database_connected',
value: () => db.isConnected() ? 1 : 0
})
// String value metric
tx2.metric({
name: 'app_version',
value: () => `v${package.version}-${process.env.NODE_ENV}`
})
// Real-time calculation
const startTime = Date.now()
tx2.metric({
name: 'uptime_hours',
unit: 'hours',
value: () => {
const uptime = Date.now() - startTime
return Math.floor(uptime / 1000 / 60 / 60)
}
})3.2. Counter
Accumulative metrics that can be incremented, decremented, and reset. Perfect for counting events and tracking totals.
// Basic counter
const requestCounter = tx2.counter('total_requests')
// Middleware integration
app.use((req, res, next) => {
requestCounter.inc()
res.on('finish', () => {
if (res.statusCode >= 400) {
errorCounter.inc()
}
if (res.statusCode >= 500) {
criticalErrorCounter.inc()
}
})
next()
})
// Counter with units
const dataTransferred = tx2.counter({
name: 'data_transferred',
unit: 'MB'
})
// Batch operations
stream.on('data', (chunk) => {
dataTransferred.inc(chunk.length / 1024 / 1024)
})
// WebSocket connection tracking
const wsConnections = tx2.counter({
name: 'websocket_connections',
unit: 'connections'
})
io.on('connection', (socket) => {
wsConnections.inc()
socket.on('disconnect', () => {
wsConnections.dec()
})
socket.on('error', () => {
wsErrors.inc()
wsConnections.dec()
})
})
// Advanced counter usage
class SessionManager {
private activeSessions = tx2.counter('active_sessions')
private totalSessions = tx2.counter('total_sessions')
createSession(userId: string): string {
const sessionId = generateSessionId()
this.activeSessions.inc()
this.totalSessions.inc()
return sessionId
}
destroySession(sessionId: string): void {
this.activeSessions.dec()
}
resetDaily(): void {
// Keep total, reset active
this.activeSessions.reset()
}
}3.3. Histogram
Statistical analysis of values over time. Provides percentiles, averages, and distribution metrics.
// Response time tracking
const responseTime = tx2.histogram({
name: 'response_time',
measurement: 'mean',
unit: 'ms'
})
app.use((req, res, next) => {
const start = Date.now()
// Monkey-patch res.end
const originalEnd = res.end
res.end = function(...args) {
const duration = Date.now() - start
responseTime.update(duration)
// Log slow requests
if (duration > 1000) {
tx2.event('slow_request', {
url: req.url,
method: req.method,
duration
})
}
return originalEnd.apply(this, args)
}
next()
})Available Measurements
// Minimum value tracking
const minLatency = tx2.histogram({
name: 'min_latency',
measurement: 'min',
unit: 'ms'
})
// Maximum value tracking
const maxMemory = tx2.histogram({
name: 'peak_memory',
measurement: 'max',
unit: 'MB'
})
// Average calculation
const avgProcessingTime = tx2.histogram({
name: 'avg_processing',
measurement: 'mean',
unit: 'ms'
})
// Median (50th percentile)
const medianQueryTime = tx2.histogram({
name: 'median_query',
measurement: 'median',
unit: 'ms'
})
// 95th percentile (95% of values are below this)
const p95ResponseTime = tx2.histogram({
name: 'p95_response',
measurement: 'p95',
unit: 'ms'
})
// 99th percentile (99% of values are below this)
const p99ResponseTime = tx2.histogram({
name: 'p99_response',
measurement: 'p99',
unit: 'ms'
})
// Variance (spread of data)
const loadVariance = tx2.histogram({
name: 'load_variance',
measurement: 'variance'
})
// Standard deviation
const jitter = tx2.histogram({
name: 'network_jitter',
measurement: 'stddev',
unit: 'ms'
})
// Exponential Moving Average (trend indicator)
const trendingValue = tx2.histogram({
name: 'trending_requests',
measurement: 'ema'
})
// Sum of all values
const totalDataProcessed = tx2.histogram({
name: 'total_data',
measurement: 'sum',
unit: 'bytes'
})
// Count of measurements
const apiCallCount = tx2.histogram({
name: 'api_calls',
measurement: 'count'
})Advanced Histogram Usage
// Database query analyzer
const queryAnalyzer = tx2.histogram({
name: 'db_query_time',
measurement: 'p99',
unit: 'ms'
})
// Hook into database events
db.on('query', (query) => {
const start = Date.now()
query.on('end', () => {
const duration = Date.now() - start
queryAnalyzer.update(duration)
// Track slow queries separately
if (duration > 100) {
slowQueryHistogram.update(duration)
}
})
})
// Full statistics retrieval
const apiStats = tx2.histogram({
name: 'api_latency',
measurement: 'mean',
unit: 'ms'
})
// Get comprehensive statistics
setInterval(() => {
const stats = apiStats.fullResults()
console.log('API Performance Stats:', {
min: `${stats.min}ms`,
max: `${stats.max}ms`,
average: `${stats.mean}ms`,
median: `${stats.median}ms`,
p95: `${stats.p95}ms`,
p99: `${stats.p99}ms`,
stdDev: `${stats.stddev}ms`,
total: stats.count
})
// Alert on degraded performance
if (stats.p95 > 500) {
tx2.event('performance:degraded', {
p95: stats.p95,
threshold: 500
})
}
}, 60000)
// Custom percentile analysis
const customPercentiles = apiStats.percentiles([0.1, 0.25, 0.5, 0.75, 0.9, 0.99])
// Returns: { 0.1: 10, 0.25: 25, 0.5: 50, 0.75: 75, 0.9: 90, 0.99: 99 }
// Memory-efficient histogram with periodic reset
let histogramUpdateCount = 0
const efficientHistogram = tx2.histogram({
name: 'efficient_metric',
measurement: 'mean'
})
function updateHistogram(value: number) {
efficientHistogram.update(value)
histogramUpdateCount++
// Reset every 10000 measurements to prevent memory growth
if (histogramUpdateCount >= 10000) {
efficientHistogram.reset()
histogramUpdateCount = 0
}
}3.4. Meter
Measures the rate of events over time using an Exponentially Weighted Moving Average (EWMA) algorithm.
// Simple request rate meter
const requestsPerSec = tx2.meter({
name: 'req/sec'
})
app.use((req, res, next) => {
requestsPerSec.mark()
next()
})
// Bandwidth meter
const bytesPerSec = tx2.meter({
name: 'bandwidth',
unit: 'MB/sec'
})
stream.on('data', (chunk) => {
// Mark with the number of bytes
bytesPerSec.mark(chunk.length / 1024 / 1024)
})
// Customized meter with different time windows
const customMeter = tx2.meter({
name: 'events/sec',
seconds: 5, // Calculate rate every 5 seconds
timeframe: 60 // Use 60-second time window for averaging
})
// Message processing rate
messageQueue.on('message', (msg) => {
customMeter.mark()
// Handle batch messages
if (msg.batch) {
customMeter.mark(msg.batch.length)
}
})
// Transaction rate tracking
const transactionRate = tx2.meter({
name: 'transactions/min',
seconds: 60,
unit: 'tx/min'
})
async function processTransaction(tx) {
transactionRate.mark()
if (tx.amount > 10000) {
largeTransactionRate.mark()
}
await db.saveTransaction(tx)
}Meter vs Counter Comparison
// Counter: Cumulative total
const totalRequests = tx2.counter('total_requests')
totalRequests.inc() // 1, 2, 3, 4, 5...
// Meter: Rate per second
const requestRate = tx2.meter({ name: 'req/sec' })
requestRate.mark() // 10 req/s, 15 req/s, 8 req/s...
// Using both together for comprehensive monitoring
app.use((req, res, next) => {
totalRequests.inc() // Track total count
requestRate.mark() // Track current rate
if (req.path.startsWith('/api')) {
apiRequests.inc()
apiRate.mark()
}
next()
})3.5. Transpose
Exposes any dynamic value as a metric. Perfect for custom calculations and external service monitoring.
// Simple value transposition
tx2.transpose('random_value', () => {
return Math.random() * 100
})
// Object notation
tx2.transpose({
name: 'complex_calculation',
data: () => {
const result = performComplexCalculation()
return Math.round(result * 100) / 100
}
})
// External service monitoring
tx2.transpose('redis_memory', async () => {
try {
const info = await redis.info('memory')
return parseInt(info.used_memory) / 1024 / 1024 // MB
} catch {
return -1 // Indicate error
}
})
// Composite metrics
tx2.transpose('efficiency_score', () => {
const hitRate = (cacheHits / (cacheHits + cacheMisses)) * 100
const errorRate = (errors / requests) * 100
const efficiency = hitRate * (1 - errorRate / 100)
return Math.round(efficiency)
})
// System status aggregation
tx2.transpose('system_health', () => {
const checks = {
database: db.isConnected(),
redis: redis.status === 'ready',
queue: queue.isRunning(),
disk: diskUsage < 90
}
const healthScore = Object.values(checks).filter(Boolean).length
return healthScore / Object.keys(checks).length * 100
})4. Issues
Comprehensive error tracking and reporting system that captures errors with full context.
// String error
tx2.issue('Critical: Database connection lost')
// Error object with stack trace
try {
await riskyDatabaseOperation()
} catch (error) {
tx2.issue(error)
}
// Enhanced error object
class CustomError extends Error {
constructor(message, code, statusCode) {
super(message)
this.code = code
this.statusCode = statusCode
this.timestamp = new Date().toISOString()
}
}
try {
await apiCall()
} catch (error) {
const enhancedError = new CustomError(
error.message,
'API_ERROR',
500
)
enhancedError.endpoint = '/api/users'
enhancedError.userId = req.user?.id
tx2.issue(enhancedError)
}
// Structured error reporting
tx2.issue({
type: 'payment_failure',
message: 'Payment processing failed',
code: 'PAYMENT_DECLINED',
severity: 'critical',
user: {
id: user.id,
email: user.email
},
payment: {
amount: 99.99,
currency: 'USD',
method: 'credit_card',
processor: 'stripe'
},
error: {
code: stripeError.code,
message: stripeError.message,
decline_code: stripeError.decline_code
},
timestamp: new Date().toISOString(),
context: {
ip: req.ip,
userAgent: req.get('user-agent'),
sessionId: req.sessionID
}
})Global Error Handlers
// Uncaught exception handler
process.on('uncaughtException', (error) => {
tx2.issue({
type: 'uncaughtException',
error: {
message: error.message,
stack: error.stack,
name: error.name
},
fatal: true,
pid: process.pid
})
// Graceful shutdown
setTimeout(() => {
process.exit(1)
}, 1000)
})
// Unhandled promise rejection
process.on('unhandledRejection', (reason, promise) => {
tx2.issue({
type: 'unhandledRejection',
reason: reason instanceof Error ? {
message: reason.message,
stack: reason.stack
} : reason,
promise: String(promise)
})
})
// Express error middleware
app.use((err, req, res, next) => {
const errorReport = {
message: err.message,
stack: err.stack,
status: err.status || 500,
request: {
url: req.url,
method: req.method,
headers: req.headers,
body: req.body,
query: req.query,
params: req.params
},
user: req.user ? {
id: req.user.id,
email: req.user.email
} : null,
session: req.sessionID,
ip: req.ip,
timestamp: new Date().toISOString()
}
tx2.issue(errorReport)
res.status(errorReport.status).json({
error: process.env.NODE_ENV === 'production'
? 'Internal Server Error'
: errorReport.message
})
})API Reference
TX2 Class
class TX2 extends EventEmitter {
// Singleton instance
constructor()
// Event Methods
event(name: string, data?: any): void
// Action Methods
action(name: string, callback: (reply: Function) => void): void
action<T>(name: string, opts: T, callback: (opts: T, reply: Function) => void): void
// Issue Methods
issue(err: string | Error | object): ErrorObject
_interpretError(err: any): ErrorObject
// Metric Methods
metric<T>(name: string, cb: () => T): Metric<T>
metric(name: string, cb: number): Metric<number>
metric<T>(name: string, unit: string, cb: () => T): Metric<T>
metric(name: string, unit: string, cb: number): Metric<number>
metric<T>(options: MetricOptions<T>): Metric<T>
counter(name: string): Counter
counter(options: CounterOptions): Counter
histogram(options: HistogramOptions): Histogram
meter(options: MeterOptions): Meter
transpose(name: string, reporter: () => any): void
transpose(options: TransposeOptions): void
// Utility Methods
metricExists(name: string): boolean
prepareData(): Record<string, MetricData>
send(args: SendArgs): void
// EventEmitter Methods
on(event: 'data', listener: (msg: TX2Message) => void): this
once(event: 'data', listener: (msg: TX2Message) => void): this
emit(event: 'data', msg: TX2Message): boolean
}Type Definitions
// Core Interfaces
interface MetricOptions<T = any> {
name: string
value?: T | (() => T)
val?: T | (() => T) // Alias for value
unit?: string
}
interface CounterOptions {
name: string
unit?: string
}
interface HistogramOptions {
name: string
measurement?: 'min' | 'max' | 'sum' | 'count' | 'mean' |
'variance' | 'stddev' | 'median' |
'p75' | 'p95' | 'p99' | 'p999' | 'ema'
unit?: string
value?: any | (() => any)
val?: any | (() => any)
}
interface MeterOptions {
name: string
seconds?: number // Tick interval in seconds (default: 1)
timeframe?: number // Time window in seconds (default: 60)
unit?: string
}
interface TransposeOptions {
name: string
data: () => any
}
interface ErrorObject {
name?: string
message?: string
stack?: string
stackframes?: Array<{
file_name: string
line_number: number
}>
[key: string]: any
}
// Metric Types
interface Metric<T = any> {
val: () => T
set: (data: T) => void
}
interface Counter {
val: () => number
inc: (amount?: number) => void
dec: (amount?: number) => void
reset: (count?: number) => void
}
interface Histogram {
update: (value: number) => void
percentiles: (percentiles: number[]) => { [key: number]: number | null }
reset: () => void
val: () => number
getMin: () => number | null
getMax: () => number | null
getSum: () => number
getCount: () => number
getEma: () => number
fullResults: () => HistogramResults
}
interface HistogramResults {
min: number | null
max: number | null
sum: number
variance: number | null
mean: number
stddev: number | null
count: number
median: number | null
p75: number | null
p95: number | null
p99: number | null
p999: number | null
ema: number
}
interface Meter {
mark: (n?: number) => void
val: () => number
}
// Message Types
type TX2EventType =
| 'axm:monitor'
| 'human:event'
| 'axm:action'
| 'axm:reply'
| 'process:exception'
interface TX2Message {
type: TX2EventType
data: any
}
interface SendArgs {
type: TX2EventType
data: any
}Advanced Usage
Building a Complete Monitoring Dashboard
// Comprehensive monitoring setup
class ApplicationMonitor {
private metrics = {
// Counters
requests: tx2.counter('http_requests_total'),
errors: tx2.counter('errors_total'),
cache_hits: tx2.counter('cache_hits'),
cache_misses: tx2.counter('cache_misses'),
// Histograms
responseTime: tx2.histogram({
name: 'http_response_time',
measurement: 'p95',
unit: 'ms'
}),
dbQueryTime: tx2.histogram({
name: 'db_query_time',
measurement: 'p99',
unit: 'ms'
}),
// Meters
throughput: tx2.meter({
name: 'throughput',
unit: 'req/sec'
}),
errorRate: tx2.meter({
name: 'error_rate',
unit: 'errors/sec'
}),
// Metrics
connections: tx2.metric({
name: 'active_connections',
value: () => this.getActiveConnections()
}),
memoryUsage: tx2.metric({
name: 'memory_usage',
unit: 'MB',
value: () => Math.round(process.memoryUsage().heapUsed / 1024 / 1024)
})
}
private getActiveConnections(): number {
return (
httpServer.connections +
wsServer.clients.size +
db.pool.activeCount
)
}
recordRequest(req: Request, res: Response, duration: number) {
this.metrics.requests.inc()
this.metrics.throughput.mark()
this.metrics.responseTime.update(duration)
if (res.statusCode >= 400) {
this.metrics.errors.inc()
this.metrics.errorRate.mark()
if (res.statusCode >= 500) {
tx2.event('http:error:5xx', {
url: req.url,
method: req.method,
status: res.statusCode,
duration
})
}
}
// Track slow requests
if (duration > 1000) {
tx2.event('http:slow_request', {
url: req.url,
duration,
threshold: 1000
})
}
}
recordCacheOperation(hit: boolean) {
if (hit) {
this.metrics.cache_hits.inc()
} else {
this.metrics.cache_misses.inc()
}
}
recordDatabaseQuery(query: string, duration: number) {
this.metrics.dbQueryTime.update(duration)
if (duration > 100) {
tx2.event('db:slow_query', {
query: query.substring(0, 200),
duration
})
}
}
}
const monitor = new ApplicationMonitor()
// Express middleware integration
app.use((req, res, next) => {
const start = Date.now()
res.on('finish', () => {
monitor.recordRequest(req, res, Date.now() - start)
})
next()
})
// Database hook
db.on('query', (query) => {
const start = Date.now()
query.on('end', () => {
monitor.recordDatabaseQuery(query.sql, Date.now() - start)
})
})
// Cache wrapper
class CacheWrapper {
async get(key: string) {
const value = await cache.get(key)
monitor.recordCacheOperation(value !== null)
return value
}
}Circuit Breaker Implementation
class CircuitBreaker {
private failures = 0
private lastFailTime = 0
private state: 'closed' | 'open' | 'half-open' = 'closed'
// Metrics
private failureCounter = tx2.counter(`circuit_breaker_${this.name}_failures`)
private stateMetric = tx2.metric({
name: `circuit_breaker_${this.name}_state`,
value: () => this.state
})
private callsMetric = tx2.histogram({
name: `circuit_breaker_${this.name}_calls`,
measurement: 'count'
})
constructor(
private name: string,
private threshold = 5,
private timeout = 60000,
private resetTimeout = 30000
) {
// Admin actions
tx2.action(`reset_circuit_${name}`, (reply) => {
this.reset()
reply({
circuit: this.name,
previousState: this.state,
newState: 'closed',
failures: 0
})
})
tx2.action(`circuit_${name}_status`, (reply) => {
reply({
name: this.name,
state: this.state,
failures: this.failures,
lastFailure: this.lastFailTime
? new Date(this.lastFailTime).toISOString()
: null,
threshold: this.threshold,
config: {
timeout: this.timeout,
resetTimeout: this.resetTimeout
}
})
})
}
async call<T>(fn: () => Promise<T>): Promise<T> {
// Check circuit state
if (this.state === 'open') {
const timeSinceFailure = Date.now() - this.lastFailTime
if (timeSinceFailure > this.timeout) {
this.state = 'half-open'
tx2.event(`circuit:half_open`, { circuit: this.name })
} else {
throw new Error(`Circuit breaker is open for ${this.name}`)
}
}
try {
const result = await fn()
this.onSuccess()
return result
} catch (error) {
this.onFailure(error)
throw error
} finally {
this.callsMetric.update(1)
}
}
private onSuccess() {
if (this.state === 'half-open') {
this.reset()
tx2.event('circuit:closed', {
circuit: this.name,
recoveredAfter: Date.now() - this.lastFailTime
})
}
}
private onFailure(error: Error) {
this.failures++
this.failureCounter.inc()
this.lastFailTime = Date.now()
if (this.failures >= this.threshold) {
this.state = 'open'
tx2.event('circuit:opened', {
circuit: this.name,
failures: this.failures,
threshold: this.threshold,
error: error.message
})
// Auto-recovery timer
setTimeout(() => {
if (this.state === 'open') {
this.state = 'half-open'
}
}, this.resetTimeout)
}
}
private reset() {
this.failures = 0
this.state = 'closed'
this.failureCounter.reset()
}
}
// Usage
const apiCircuit = new CircuitBreaker('external_api', 5, 60000)
async function callExternalAPI(data) {
return apiCircuit.call(async () => {
const response = await fetch('https://api.example.com', {
method: 'POST',
body: JSON.stringify(data)
})
if (!response.ok) {
throw new Error(`API returned ${response.status}`)
}
return response.json()
})
}Memory Leak Detection
class MemoryLeakDetector {
private samples: number[] = []
private increasing = 0
private threshold = 10
private memoryHistogram = tx2.histogram({
name: 'memory_heap_used',
measurement: 'max',
unit: 'MB'
})
private leakMetric = tx2.metric({
name: 'memory_leak_probability',
unit: '%',
value: () => this.getLeakProbability()
})
constructor() {
// Sample memory every 10 seconds
setInterval(() => this.sample(), 10000)
// Force GC action if available
if (global.gc) {
tx2.action('force_gc', (reply) => {
const before = process.memoryUsage()
global.gc()
const after = process.memoryUsage()
reply({
freed: {
heap: (before.heapUsed - after.heapUsed) / 1024 / 1024,
external: (before.external - after.external) / 1024 / 1024,
total: ((before.heapUsed - after.heapUsed) +
(before.external - after.external)) / 1024 / 1024
},
unit: 'MB',
before: this.formatMemory(before),
after: this.formatMemory(after)
})
})
}
// Memory snapshot action
tx2.action('memory_snapshot', (reply) => {
const mem = process.memoryUsage()
const heap = v8.getHeapStatistics()
reply({
process: this.formatMemory(mem),
heap: {
total: heap.total_heap_size / 1024 / 1024,
executable: heap.total_heap_size_executable / 1024 / 1024,
physical: heap.total_physical_size / 1024 / 1024,
available: heap.total_available_size / 1024 / 1024,
used: heap.used_heap_size / 1024 / 1024,
limit: heap.heap_size_limit / 1024 / 1024,
malloced: heap.malloced_memory / 1024 / 1024,
peak_malloced: heap.peak_malloced_memory / 1024 / 1024
},
samples: this.samples.slice(-20),
trend: this.getTrend(),
leakProbability: this.getLeakProbability()
})
})
}
private sample() {
const mem = process.memoryUsage()
const heapMB = mem.heapUsed / 1024 / 1024
this.samples.push(heapMB)
if (this.samples.length > 100) {
this.samples.shift()
}
this.memoryHistogram.update(heapMB)
// Detect consistent increases
if (this.samples.length > 1) {
const last = this.samples[this.samples.length - 1]
const prev = this.samples[this.samples.length - 2]
if (last > prev) {
this.increasing++
// Alert on potential leak
if (this.increasing > this.threshold) {
tx2.event('memory:potential_leak', {
current: heapMB,
samples: this.samples.slice(-10),
trend: 'increasing',
consecutiveIncreases: this.increasing
})
}
} else {
this.increasing = Math.max(0, this.increasing - 1)
}
}
// Check absolute threshold
if (heapMB > 500) {
tx2.event('memory:high_usage', {
current: heapMB,
threshold: 500,
unit: 'MB'
})
}
}
private getTrend(): 'increasing' | 'stable' | 'decreasing' {
if (this.samples.length < 10) return 'stable'
const recent = this.samples.slice(-10)
const avg = recent.reduce((a, b) => a + b, 0) / recent.length
const older = this.samples.slice(-20, -10)
const oldAvg = older.reduce((a, b) => a + b, 0) / older.length
const change = (avg - oldAvg) / oldAvg * 100
if (change > 10) return 'increasing'
if (change < -10) return 'decreasing'
return 'stable'
}
private getLeakProbability(): number {
if (this.samples.length < 20) return 0
// Calculate linear regression slope
const n = this.samples.length
const sumX = (n * (n + 1)) / 2
const sumY = this.samples.reduce((a, b) => a + b, 0)
const sumXY = this.samples.reduce((sum, y, x) => sum + x * y, 0)
const sumX2 = (n * (n + 1) * (2 * n + 1)) / 6
const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX)
// Positive slope indicates potential leak
return Math.min(100, Math.max(0, slope * 100))
}
private formatMemory(mem: NodeJS.MemoryUsage) {
return {
rss: Math.round(mem.rss / 1024 / 1024),
heapTotal: Math.round(mem.heapTotal / 1024 / 1024),
heapUsed: Math.round(mem.heapUsed / 1024 / 1024),
external: Math.round(mem.external / 1024 / 1024),
arrayBuffers: Math.round(mem.arrayBuffers / 1024 / 1024)
}
}
}
// Initialize detector
const memoryDetector = new MemoryLeakDetector()Database Connection Pool Monitor
class DatabasePoolMonitor {
private queryHistogram = tx2.histogram({
name: 'db_query_duration',
measurement: 'p99',
unit: 'ms'
})
private connectionHistogram = tx2.histogram({
name: 'db_connection_wait',
measurement: 'mean',
unit: 'ms'
})
constructor(private pool: Pool) {
// Pool metrics
tx2.metric({
name: 'db_pool_total',
value: () => pool.totalCount
})
tx2.metric({
name: 'db_pool_idle',
value: () => pool.idleCount
})
tx2.metric({
name: 'db_pool_active',
value: () => pool.totalCount - pool.idleCount
})
tx2.metric({
name: 'db_pool_waiting',
value: () => pool.waitingCount
})
// Pool events
pool.on('acquire', (client) => {
const waitTime = Date.now() - client._poolAcquireTime
this.connectionHistogram.update(waitTime)
if (waitTime > 1000) {
tx2.event('db:slow_acquire', {
waitTime,
poolSize: pool.totalCount,
waiting: pool.waitingCount
})
}
})
pool.on('release', () => {
// Track connection lifecycle
})
pool.on('error', (err, client) => {
tx2.issue({
type: 'database_pool_error',
error: err.message,
code: err.code,
client: client._clientId,
pool: {
total: pool.totalCount,
idle: pool.idleCount,
waiting: pool.waitingCount
}
})
})
// Pool management actions
tx2.action('db_pool_status', (reply) => {
reply({
config: {
min: pool.options.min,
max: pool.options.max,
idleTimeout: pool.options.idleTimeoutMillis,
connectionTimeout: pool.options.connectionTimeoutMillis
},
current: {
total: pool.totalCount,
idle: pool.idleCount,
active: pool.totalCount - pool.idleCount,
waiting: pool.waitingCount
},
statistics: {
queryTime: this.queryHistogram.fullResults(),
connectionWait: this.connectionHistogram.fullResults()
}
})
})
tx2.action('db_pool_resize',
{ min: 2, max: 10 },
async (opts, reply) => {
try {
await pool.resize(opts.min, opts.max)
reply({
success: true,
newSize: {
min: opts.min,
max: opts.max,
current: pool.totalCount
}
})
} catch (error) {
reply({
success: false,
error: error.message
})
}
}
)
}
async query(text: string, params?: any[]): Promise<any> {
const start = Date.now()
const client = await this.pool.connect()
try {
const result = await client.query(text, params)
const duration = Date.now() - start
this.queryHistogram.update(duration)
if (duration > 100) {
tx2.event('db:slow_query', {
query: text.substring(0, 100),
duration,
rows: result.rowCount
})
}
return result
} catch (error) {
tx2.issue({
type: 'query_error',
query: text,
params,
error: error.message,
code: error.code
})
throw error
} finally {
client.release()
}
}
}
// Usage
const dbMonitor = new DatabasePoolMonitor(pgPool)
// Use monitored query method
app.get('/users', async (req, res) => {
try {
const result = await dbMonitor.query(
'SELECT * FROM users WHERE active = $1',
[true]
)
res.json(result.rows)
} catch (error) {
res.status(500).json({ error: 'Database error' })
}
})Technical Details
Monitoring Interval Architecture
TX2 uses a fixed 990ms interval for metric transmission. This design choice:
- Network Latency Compensation: The 10ms buffer ensures metrics arrive before the 1-second mark
- Dashboard Synchronization: Aligns with PM2 dashboard's 1-second refresh rate
- CPU Optimization: Prevents excessive CPU usage from too frequent updates
- Batching Efficiency: All metrics are collected and sent in a single message
// Internal implementation
private _monitorInterval = setInterval(() => {
this.send({
type: 'axm:monitor',
data: this.prepareData() // Collects all metric values
})
}, 990)
// The interval is "unref'd" to not keep the process alive
this._monitorInterval.unref()Message Buffering Strategy
TX2 implements a no-buffer strategy for optimal real-time monitoring:
- Events: Sent immediately when
event()is called - Issues: Transmitted instantly upon
issue()call - Actions: Replies sent immediately when callback executes
- Metrics: Only metrics are batched and sent every 990ms
This ensures critical events and errors are never delayed or lost.
Memory Management Strategies
Histogram Memory Control
// Problem: Histograms store all values indefinitely
const histogram = tx2.histogram({
name: 'api_calls',
measurement: 'mean'
})
// Each update adds to internal array
histogram.update(100) // stores [100]
histogram.update(200) // stores [100, 200]
// After 1M calls: stores [100, 200, ..., 1000000 values]
// Solution 1: Periodic reset
class ManagedHistogram {
private histogram = tx2.histogram(this.options)
private updateCount = 0
private maxSamples = 10000
update(value: number) {
this.histogram.update(value)
this.updateCount++
if (this.updateCount >= this.maxSamples) {
// Store summary before reset
const summary = this.histogram.fullResults()
tx2.event('histogram:reset', {
name: this.options.name,
summary
})
this.histogram.reset()
this.updateCount = 0
}
}
}
// Solution 2: Sliding window
class SlidingWindowHistogram {
private values: number[] = []
private maxSize = 1000
update(value: number) {
this.values.push(value)
if (this.values.length > this.maxSize) {
this.values.shift() // Remove oldest
}
// Update actual histogram with windowed data
this.histogram.reset()
this.values.forEach(v => this.histogram.update(v))
}
}Action Listener Management
TX2's improved implementation prevents memory leaks from duplicate action registrations:
// Internal implementation uses a Map to store single listener per action
private _actionListeners: Map<string, Function> = new Map()
action(name: string, handler: Function) {
// Replaces existing handler if present
this._actionListeners.set(name, handler)
// Single global listener handles all actions
if (!this._globalListenerRegistered) {
process.on('message', (data) => {
for (const [actionName, handler] of this._actionListeners) {
if (data === actionName || data.msg === actionName) {
handler(data)
}
}
})
this._globalListenerRegistered = true
}
}
// Safe to call multiple times
for (let i = 0; i < 100; i++) {
tx2.action('test', handler) // Only one listener remains
}Error Serialization Deep Dive
TX2's error handling preserves all error properties:
// How TX2 serializes errors
function serializeError(err: any): object {
if (typeof err === 'string') {
return { message: err, stack: err }
}
if (err instanceof Error) {
// Get all properties including non-enumerable
const props = Object.getOwnPropertyNames(err)
const serialized: any = {}
props.forEach(prop => {
serialized[prop] = err[prop]
})
// Handle special Error properties
if (err.stack) serialized.stack = err.stack
if (err.message) serialized.message = err.message
if (err.name) serialized.name = err.name
// Handle custom properties
if (err.__error_callsites) {
serialized.stackframes = err.__error_callsites.map(site => ({
file_name: site.getFileName(),
line_number: site.getLineNumber()
}))
}
return serialized
}
return err // Already an object
}
// Usage example
class CustomError extends Error {
code: string
statusCode: number
details: any
constructor(message: string, code: string, statusCode: number) {
super(message)
this.code = code
this.statusCode = statusCode
this.name = 'CustomError'
}
}
const error = new CustomError('Not found', 'RESOURCE_NOT_FOUND', 404)
error.details = { resource: 'user', id: 123 }
tx2.issue(error)
// Sends: {
// message: 'Not found',
// stack: '...',
// name: 'CustomError',
// code: 'RESOURCE_NOT_FOUND',
// statusCode: 404,
// details: { resource: 'user', id: 123 }
// }Circular Reference Handling
TX2 uses fast-safe-stringify to handle circular references:
// Example of circular reference
const user = { id: 1, name: 'John' }
const post = { id: 1, title: 'Hello', author: user }
user.posts = [post] // Circular reference
// Safe to send
tx2.event('data:circular', user)
// Serializes to: {"id":1,"name":"John","posts":[{"id":1,"title":"Hello","author":"[Circular]"}]}
// Complex circular structures
const obj1 = { name: 'obj1' }
const obj2 = { name: 'obj2' }
const obj3 = { name: 'obj3' }
obj1.ref = obj2
obj2.ref = obj3
obj3.ref = obj1 // Three-way circular
tx2.event('complex:circular', obj1)
// Safely serialized without infinite recursionStatistical Algorithms
Welford's Algorithm for Variance
TX2 uses Welford's online algorithm for numerically stable variance calculation:
// Welford's algorithm implementation
class WelfordVariance {
private n = 0
private mean = 0
private M2 = 0
update(value: number) {
this.n++
const delta = value - this.mean
this.mean += delta / this.n
const delta2 = value - this.mean
this.M2 += delta * delta2
}
get variance() {
return this.n < 2 ? 0 : this.M2 / (this.n - 1)
}
get stddev() {
return Math.sqrt(this.variance)
}
}Exponentially Weighted Moving Average (EWMA)
Used in meters for rate calculation:
// EWMA implementation
class EWMA {
private rate = 0
private uncounted = 0
private alpha: number
constructor(
private interval: number, // Tick interval
private timeframe: number // Time window
) {
this.alpha = 1 - Math.exp(-interval / timeframe)
}
update(n: number) {
this.uncounted += n
}
tick() {
const instantRate = this.uncounted / (this.interval / 1000)
this.uncounted = 0
// Exponential decay
this.rate += this.alpha * (instantRate - this.rate)
}
getRate(): number {
return this.rate
}
}
// Usage in Meter
const ewma = new EWMA(1000, 60000) // 1s tick, 1min window
setInterval(() => ewma.tick(), 1000)
// Mark events
ewma.update(1) // Event occurred
ewma.getRate() // Current ratePerformance Considerations
Metric Function Performance
// ❌ Bad - Heavy computation in metric function
tx2.metric({
name: 'expensive_metric',
value: () => {
// This runs every 990ms!
let sum = 0
for (let i = 0; i < 1000000; i++) {
sum += Math.sqrt(i)
}
return sum
}
})
// ✅ Good - Cache expensive calculations
let cachedValue = 0
const updateInterval = setInterval(() => {
let sum = 0
for (let i = 0; i < 1000000; i++) {
sum += Math.sqrt(i)
}
cachedValue = sum
}, 10000) // Update every 10 seconds
tx2.metric({
name: 'expensive_metric',
value: () => cachedValue // Fast retrieval
})Event Throttling
// ❌ Bad - Unthrottled events
app.use((req, res, next) => {
tx2.event('request', { url: req.url }) // Fires for every request!
next()
})
// ✅ Good - Throttled events
const throttle = (fn: Function, delay: number) => {
let last = 0
return (...args: any[]) => {
const now = Date.now()
if (now - last >= delay) {
last = now
fn(...args)
}
}
}
const throttledEvent = throttle((data) => {
tx2.event('request_batch', data)
}, 1000)
let requestBatch: any[] = []
app.use((req, res, next) => {
requestBatch.push({ url: req.url, method: req.method })
if (requestBatch.length >= 100) {
throttledEvent({ count: requestBatch.length, requests: requestBatch })
requestBatch = []
}
next()
})Histogram Optimization
// Optimized histogram with automatic memory management
class OptimizedHistogram {
private histogram: Histogram
private sampleCount = 0
private summaryStats = {
total: 0,
min: Infinity,
max: -Infinity
}
constructor(
private options: HistogramOptions,
private maxSamples = 1000
) {
this.histogram = tx2.histogram(options)
}
update(value: number) {
// Always track summary stats
this.summaryStats.total++
this.summaryStats.min = Math.min(this.summaryStats.min, value)
this.summaryStats.max = Math.max(this.summaryStats.max, value)
// Sample for detailed stats
if (this.sampleCount < this.maxSamples) {
this.histogram.update(value)
this.sampleCount++
} else {
// Reservoir sampling for even distribution
const rand = Math.floor(Math.random() * this.summaryStats.total)
if (rand < this.maxSamples) {
this.histogram.reset()
this.histogram.update(value)
this.sampleCount = 1
}
}
}
}Troubleshooting
1. Metrics Not Appearing in PM2 Dashboard
// Check if running under PM2
if (!process.send) {
console.warn('WARNING: Not running under PM2 - metrics will not be sent')
console.warn('Start with: pm2 start app.js')
}
// Verify PM2 configuration
// ecosystem.config.js
module.exports = {
apps: [{
name: 'my-app',
script: './dist/app.js',
instances: 1,
exec_mode: 'fork', // Required for IPC
max_memory_restart: '500M',
// Enable monitoring
pmx: true,
instance_var: 'INSTANCE_ID',
// Error handling
error_file: './logs/err.log',
out_file: './logs/out.log',
merge_logs: true,
time: true
}]
}
// Debug TX2 messages
tx2.on('data', (msg) => {
console.log('[TX2 Debug]', JSON.stringify(msg))
})2. Actions Not Triggering
// Debug action registration
const originalAction = tx2.action.bind(tx2)
tx2.action = function(name: string, ...args: any[]) {
console.log(`[Action Registered] ${name}`)
return originalAction(name, ...args)
}
// Test action
tx2.action('test_action', (reply) => {
console.log('[Action Triggered] test_action')
reply({
success: true,
timestamp: Date.now(),
pid: process.pid
})
})
// Monitor incoming messages
process.on('message', (msg) => {
console.log('[IPC Message]', msg)
})3. Memory Leak Warnings
// Warning: Possible EventEmitter memory leak detected
(tx2 as any).setMaxListeners(50)
// Or globally
require('events').EventEmitter.defaultMaxListeners = 50
// Track listener count
console.log('TX2 Listeners:', tx2.listenerCount('data'))
console.log('Process Listeners:', process.listenerCount('message'))
// Clean up histograms periodically
class HistogramManager {
private histograms: Map<string, Histogram> = new Map()
create(options: HistogramOptions): Histogram {
const histogram = tx2.histogram(options)
this.histograms.set(options.name, histogram)
// Auto-cleanup timer
setInterval(() => {
if (histogram.getCount() > 10000) {
console.log(`Resetting histogram: ${options.name}`)
histogram.reset()
}
}, 300000) // Every 5 minutes
return histogram
}
cleanup() {
this.histograms.forEach(h => h.reset())
this.histograms.clear()
}
}4. High CPU Usage
// Profile metric functions
const metricsProfile: Map<string, number> = new Map()
function profiledMetric(options: MetricOptions) {
const name = options.name
const originalValue = options.value
if (typeof originalValue === 'function') {
options.value = () => {
const start = process.hrtime.bigint()
const result = originalValue()
const duration = Number(process.hrtime.bigint() - start) / 1000000 // ms
metricsProfile.set(name, duration)
if (duration > 10) {
console.warn(`[Slow Metric] ${name} took ${duration}ms`)
}
return result
}
}
return tx2.metric(options)
}
// Monitor metric performance
setInterval(() => {
const slowMetrics = Array.from(metricsProfile.entries())
.filter(([_, duration]) => duration > 5)
.sort((a, b) => b[1] - a[1])
if (slowMetrics.length > 0) {
console.log('Slow Metrics:', slowMetrics)
}
}, 60000)5. TypeScript Configuration
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
// Strict settings
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
// Module resolution
"moduleResolution": "node",
"resolveJsonModule": true,
// Source maps for debugging
"sourceMap": true,
"inlineSources": true,
// Decorators if needed
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
// Type roots
"types": ["node"],
"typeRoots": ["./node_modules/@types", "./types"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test"]
}
// Type declarations for TX2
// types/tx2.d.ts
declare module 'tx2' {
// ... (include all interfaces from this document)
}Best Practices Summary
✅ Do's
- Meaningful Naming: Use clear namespace:action format for events
- Specify Units: Always define units for metrics
- Error Handling: Wrap all actions in try-catch blocks
- Memory Management: Periodically reset histograms with high volume
- Event Throttling: Implement throttling for high-frequency events
- Cache Calculations: Cache expensive metric calculations
- Graceful Shutdown: Clean up metrics and send final events on shutdown
- Debug Mode: Add debug logging in development
- Monitor Performance: Track metric calculation times
- Document Actions: Provide clear action descriptions and expected parameters
❌ Don'ts
- Heavy Computations: Avoid expensive operations in metric value functions
- Excessive Events: Don't send events for every single operation
- Large Payloads: Avoid sending huge objects in events or action replies
- Synchronous I/O: Never use sync I/O in metric functions
- Global State Reliance: Don't depend on mutable global state in actions
- Unhandled Promises: Always handle promise rejections in actions
- Memory Leaks: Don't store unlimited data in memory
- Tight Loops: Avoid tight loops in metric calculations
- Blocking Operations: Never block the event loop in metrics
- Sensitive Data: Never log passwords or tokens in events
Complete Real-World Example
// Complete e-commerce monitoring setup
import tx2 from './tx2'
import express from 'express'
import { Pool } from 'pg'
import Redis from 'ioredis'
const app = express()
const db = new Pool({ /* config */ })
const redis = new Redis()
const cache = new Map()
// Initialize monitoring
class EcommerceMonitor {
// Counters
private orders = tx2.counter('orders_total')
private revenue = tx2.counter({ name: 'revenue_total', unit: 'USD' })
private refunds = tx2.counter({ name: 'refunds_total', unit: 'USD' })
private cartAbandoned = tx2.counter('carts_abandoned')
// Histograms
private cartValue = tx2.histogram({
name: 'cart_value',
measurement: 'mean',
unit: 'USD'
})
private checkoutDuration = tx2.histogram({
name: 'checkout_duration',
measurement: 'p95',
unit: 'seconds'
})
private searchLatency = tx2.histogram({
name: 'search_latency',
measurement: 'p99',
unit: 'ms'
})
// Meters
private orderRate = tx2.meter({
name: 'order_rate',
unit: 'orders/min',
seconds: 60
})
private searchRate = tx2.meter({
name: 'search_rate',
unit: 'searches/sec'
})
// Metrics
private conversionRate = tx2.metric({
name: 'conversion_rate',
unit: '%',
value: () => {
const rate = (this.orders.val() / this.sessions.val()) * 100
return Math.round(rate * 100) / 100
}
})
private avgOrderValue = tx2.metric({
name: 'average_order_value',
unit: 'USD',
value: () => {
const orders = this.orders.val()
if (orders === 0) return 0
return Math.round(this.revenue.val() / orders * 100) / 100
}
})
private inventoryLow = tx2.metric({
name: 'inventory_low_items',
value: async () => {
const result = await db.query(
'SELECT COUNT(*) FROM products WHERE stock < 10'
)
return result.rows[0].count
}
})
// Sessions tracking
private sessions = tx2.counter('sessions_total')
private activeSessions = new Set<string>()
// Actions
constructor() {
// Daily reset action
tx2.action('reset_daily_metrics', (reply) => {
const previousData = {
orders: this.orders.val(),
revenue: this.revenue.val(),
sessions: this.sessions.val()
}
this.orders.reset()
this.revenue.reset()
this.sessions.reset()
this.cartAbandoned.reset()
reply({
reset: true,
previousData,
timestamp: new Date().toISOString()
})
})
// Cache management
tx2.action('clear_cache', async (reply) => {
try {
await redis.flushdb()
cache.clear()
reply({
success: true,
cleared: {
redis: true,
memory: true
}
})
} catch (error) {
reply({
success: false,
error: error.message
})
}
})
// Inventory check
tx2.action('inventory_report', async (reply) => {
const [low, outOfStock, total] = await Promise.all([
db.query('SELECT * FROM products WHERE stock < 10'),
db.query('SELECT * FROM products WHERE stock = 0'),
db.query('SELECT COUNT(*) FROM products')
])
reply({
total: total.rows[0].count,
lowStock: low.rows.map(p => ({
id: p.id,
name: p.name,
stock: p.stock
})),
outOfStock: outOfStock.rows.map(p => ({
id: p.id,
name: p.name
})),
critical: outOfStock.rows.length > 0
})
})
// Performance snapshot
tx2.action('performance_snapshot', (reply) => {
reply({
metrics: {
orders: this.orders.val(),
revenue: this.revenue.val(),
avgOrderValue: this.avgOrderValue.val(),
conversionRate: this.conversionRate.val()
},
performance: {
checkoutTime: this.checkoutDuration.fullResults(),
searchLatency: this.searchLatency.fullResults()
},
rates: {
orders: this.orderRate.val(),
searches: this.searchRate.val()
},
system: {
memory: process.memoryUsage(),
uptime: process.uptime(),
connections: {
database: db.totalCount,
redis: redis.status === 'ready'
}
}
})
})
}
// Track order
recordOrder(order: any) {
this.orders.inc()
this.revenue.inc(order.total)
this.orderRate.mark()
this.cartValue.update(order.total)
tx2.event('order:completed', {
orderId: order.id,
total: order.total,
items: order.items.length,
customer: order.customerId,
paymentMethod: order.paymentMethod
})
// Track high-value orders
if (order.total > 1000) {
tx2.event('order:high_value', {
orderId: order.id,
total: order.total,
customer: order.customerId
})
}
}
// Track search
recordSearch(query: string, results: number, duration: number) {
this.searchRate.mark()
this.searchLatency.update(duration)
if (results === 0) {
tx2.event('search:no_results', { query })
}
if (duration > 500) {
tx2.event('search:slow', {
query,
duration,
results
})
}
}
// Track checkout
recordCheckout(sessionId: string, duration: number, success: boolean) {
if (success) {
this.checkoutDuration.update(duration / 1000)
} else {
this.cartAbandoned.inc()
tx2.event('checkout:abandoned', {
sessionId,
duration,
stage: 'payment'
})
}
}
// Session management
startSession(sessionId: string) {
this.sessions.inc()
this.activeSessions.add(sessionId)
}
endSession(sessionId: string) {
this.activeSessions.delete(sessionId)
}
}
// Initialize monitor
const monitor = new EcommerceMonitor()
// Middleware
app.use((req, res, next) => {
// Session tracking
if (!req.session.started) {
monitor.startSession(req.sessionID)
req.session.started = true
}
next()
})
// Routes
app.post('/api/orders', async (req, res) => {
const startTime = Date.now()
try {
const order = await processOrder(req.body)
const duration = Date.now() - startTime
monitor.recordOrder(order)
monitor.recordCheckout(req.sessionID, duration, true)
res.json({ success: true, order })
} catch (error) {
monitor.recordCheckout(req.sessionID, Date.now() - startTime, false)
tx2.issue({
type: 'order_processing_error',
error: error.message,
sessionId: req.sessionID,
cart: req.body
})
res.status(500).json({ error: 'Order processing failed' })
}
})
app.get('/api/search', async (req, res) => {
const start = Date.now()
try {
const results = await searchProducts(req.query.q)
const duration = Date.now() - start
monitor.recordSearch(req.query.q, results.length, duration)
res.json({ results, count: results.length })
} catch (error) {
tx2.issue({
type: 'search_error',
query: req.query.q,
error: error.message
})
res.status(500).json({ error: 'Search failed' })
}
})
// Startup
app.listen(3000, () => {
tx2.event('app:started', {
port: 3000,
environment: process.env.NODE_ENV,
version: process.env.npm_package_version,
node: process.version
})
console.log('E-commerce API running on port 3000')
})
// Graceful shutdown
process.on('SIGTERM', async () => {
tx2.event('app:shutdown:started'