@periodic/arsenic
v1.0.1
Published
A semantic runtime monitoring library for database queries with request correlation and intelligent signal detection.
Maintainers
Readme
🔬 Periodic Arsenic
Production-grade, framework-agnostic semantic runtime monitoring library for Node.js with TypeScript support
Part of the Periodic series of Node.js packages by Uday Thakur.
💡 Why Arsenic?
Arsenic gets its name from the chemical element known for its dual nature — both toxic and medicinal depending on concentration. Just like how arsenic in the right hands reveals what's harmful, this library reveals what's breaking your system — turning invisible performance problems into clear, actionable signals.
In chemistry, arsenic is a metalloid renowned for its sensitivity in trace detection, historically used to uncover hidden threats. Similarly, @periodic/arsenic was forged through real-world production experience to surface the hidden signals your backend is already emitting — before they become incidents.
The name represents:
- Detection: Surfaces signals invisible to traditional monitoring
- Precision: Semantic judgments, not raw numbers
- Awareness: Request-aware context for every database query
- Clarity: Explains why a system is slow, not just that it is slow
Just as arsenic is a powerful diagnostic tool in the right hands, @periodic/arsenic serves as the foundational observability layer for production-grade Node.js applications.
🎯 Why Choose Arsenic?
Building robust backends requires understanding performance in context, but most observability solutions fall short:
- Metrics tools tell you that something is slow, not why
- APM vendors lock you into proprietary dashboards and formats
- Manual logging is inconsistent and misses cross-cutting patterns
- Query analyzers work in isolation without request context
- Tracing tools require heavy instrumentation and steep learning curves
Periodic Arsenic provides the perfect solution:
✅ Zero dependencies — Pure TypeScript monitoring core
✅ Framework-agnostic — Safe for use in both libraries and applications
✅ 30+ Production Signals — Critical, Warning, and Info classifications
✅ Request Correlation — Every query linked to the HTTP request that triggered it
✅ Multi-Database — Prisma, Mongoose, PostgreSQL, and Redis support
✅ Multi-Framework — Express and Fastify ready
✅ OpenTelemetry — Built-in OTEL exporter
✅ Callsite Attribution — Know exactly which file and line triggered a query
✅ Semantic Explanations — Human-readable descriptions of every signal
✅ Redis Command Monitoring — 29 commands categorized with docs links
✅ Type-safe — Strict TypeScript from the ground up
✅ No global state — No side effects on import
✅ Production-ready — Non-blocking, never crashes your app
📦 Installation
npm install @periodic/arsenicOr with yarn:
yarn add @periodic/arsenicOptional peer dependencies (install only what you need):
# Database adapters
npm install mongoose # For MongoDB
npm install @prisma/client # For SQL via Prisma
npm install pg # For PostgreSQL
npm install redis ioredis # For Redis
# Framework support
npm install express # For Express
npm install fastify # For Fastify
# Exporters
npm install @opentelemetry/api # For OpenTelemetry🚀 Quick Start
import express from 'express';
import mongoose from 'mongoose';
import {
createMonitor,
expressContext,
mongooseAdapter,
} from '@periodic/arsenic';
const app = express();
// 1. Create monitor
const monitor = createMonitor({
slowQueryThresholdMs: 200,
exporter: (event) => {
console.log(JSON.stringify(event, null, 2));
},
});
// 2. Attach Express context
app.use(expressContext(monitor));
// 3. Instrument Mongoose
mongooseAdapter(monitor, mongoose);
// 4. Write normal app code — Arsenic observes automatically
app.get('/users/:id', async (req, res) => {
const user = await User.findOne({ _id: req.params.id });
res.json(user);
});
app.listen(3000);Example Event Output:
{
"type": "db.query",
"db": "mongodb",
"adapter": "mongoose",
"model": "User",
"operation": "findOne",
"durationMs": 312,
"slow": true,
"signals": ["hot_path", "unbounded_query"],
"severity": "critical",
"explanations": {
"hot_path": {
"summary": "This query is both slow and frequently executed.",
"detail": "It appears on a hot execution path and has outsized impact on overall latency.",
"severity": "critical"
}
},
"request": {
"id": "req_8f29",
"method": "GET",
"route": "/users/:id"
},
"callsite": {
"file": "src/routes/users.ts",
"line": 14
},
"timestamp": "2024-02-13T10:30:00.000Z"
}🧠 Core Concepts
The createMonitor Function
createMonitoris the primary factory function- Returns a configured
Monitorinstance - Accepts flexible configuration options
- This is the main entry point for all applications
- No global state, safe for multi-tenant apps
Typical usage:
- Application code creates monitors with
createMonitor() - Database adapters attach to the monitor for instrumentation
- Framework middleware attaches request context automatically
- Exporters receive structured events for routing to your stack
const monitor = createMonitor({
slowQueryThresholdMs: 200,
emitPositiveSignals: false,
includeDocs: true,
exporter: (event) => console.log(event),
});The Monitor Class
Monitoris the core observation engine- Correlates database queries to active HTTP requests
- Detects 30+ semantic signals across three severity levels
- Never blocks — all exports are asynchronous and safe
- Callsite attribution pinpoints the exact source of every query
Design principle:
Applications create monitors, adapters observe queries, exporters decide where data goes.
// Signal-based routing
exporter: (event) => {
if (event.severity === 'critical') sendToPagerDuty(event);
else if (event.severity === 'warning') sendToSlack(event);
else logger.info(event);
},✨ Features
🔴 Critical Signals (16)
High-impact issues that require immediate attention:
hot_path — Slow query on a frequently hit execution path
n_plus_one — Multiple queries where one should suffice
unbounded_query — Missing LIMIT clause on large collections
blocking_io — Event loop blocking operations detected
retry_loop — Excessive query retries
write_contention — High-frequency writes to the same record
connection_pool_exhaustion — Pool at capacity⚠️ Warning Signals (9)
Issues worth tracking before they escalate:
slow_query — Exceeded configured threshold
fan_out — Too many queries per request
high_variance_latency — Inconsistent performance across requests
high_cpu — Elevated CPU usage during query
high_memory — Elevated memory usage
large_payload — Excessive data returned from query
deprecated_api — Using deprecated database methods
overfetching — Selecting unnecessary fields
read_heavy_hotspot — Concentrated read activity on a single recordℹ️ Info Signals (14 — opt-in)
Positive signals confirming healthy patterns:
fast_query — Well under the configured threshold
bounded_query — Proper LIMIT usage detected
indexed_lookup — Efficient index usage confirmed
stable_latency — Consistent performance across requests
cached_query — Cache hit detected
index_hit — Confirmed index usage
single_query — No N+1 patterns detected
optimized_join — Efficient joins in use
connection_reused — Connection pool working well
low_memory — Memory efficient query
low_cpu — CPU efficient operation
stable_response — Matches performance benchmarks
cache_candidate — Query would benefit from caching
healthy_hot_path — Optimized hot path performing wellEnable info signals:
const monitor = createMonitor({
emitPositiveSignals: true,
exporter: (event) => {
if (event.signals.includes('cache_candidate')) {
console.log('Consider caching:', event.model);
}
},
});🔗 Request Correlation
Every database query is automatically linked to the HTTP request that triggered it:
// Express middleware attaches context automatically
app.use(expressContext(monitor));
// Now every query inside a request handler is correlated
app.get('/api/orders', async (req, res) => {
const orders = await Order.find({ userId: req.user.id });
// Event will include request.method, request.route, request.id
res.json(orders);
});Event output:
{
"request": {
"id": "req_8f29",
"method": "GET",
"route": "/api/orders",
"userId": "user_123"
}
}📍 Callsite Attribution
Know exactly which file and line triggered a slow query:
{
"callsite": {
"file": "src/services/OrderService.ts",
"line": 47
}
}🔌 Adapters
Mongoose
import mongoose from 'mongoose';
import { mongooseAdapter } from '@periodic/arsenic';
mongooseAdapter(monitor, mongoose);Prisma
import { PrismaClient } from '@prisma/client';
import { prismaAdapter } from '@periodic/arsenic';
const prisma = new PrismaClient();
prismaAdapter(monitor, prisma);PostgreSQL (pg)
import { Pool } from 'pg';
import { pgAdapter } from '@periodic/arsenic';
const pool = new Pool();
pgAdapter(monitor, pool);Redis (ioredis / redis)
import Redis from 'ioredis';
import {
createMonitor,
redisAdapter,
SLOW_REDIS_COMMANDS,
getRedisCommandInfo,
REDIS_COMMAND_INFO,
} from '@periodic/arsenic';
const redis = new Redis();
const monitor = createMonitor({
slowQueryThresholdMs: 50,
exporter: (event) => {
// Route by command category for fine-grained alerting
const category = event.metadata?.commandCategory;
if (category === 'dangerous') {
console.error('[DANGEROUS REDIS COMMAND]', event);
sendToPagerDuty(event);
} else if (category === 'blocking') {
console.warn('[BLOCKING REDIS COMMAND]', event);
} else if (SLOW_REDIS_COMMANDS.includes(event.operation)) {
console.warn('[SLOW REDIS COMMAND]', event);
}
},
});
redisAdapter(monitor, redis);Redis Command Categories
Arsenic automatically classifies every Redis command into one of four categories, giving you the context to route alerts appropriately:
| Category | Count | Description |
|----------|-------|-------------|
| dangerous | 3 | Commands that should never run in production — they block the server or destroy data |
| blocking | 4 | Commands that block until data is available — use with care |
| slow | 22 | Commands with O(N) or worse complexity that can degrade under load |
| normal | default | All other commands — no special classification |
Example — using getRedisCommandInfo():
import { getRedisCommandInfo } from '@periodic/arsenic';
const info = getRedisCommandInfo('KEYS');
console.log(info);
// { command: 'KEYS', category: 'dangerous', docs: 'https://periodic.dev/redis/keys' }
const info2 = getRedisCommandInfo('GET');
console.log(info2);
// { command: 'GET', category: 'normal', docs: 'https://periodic.dev/redis/get' }Example — list all monitored commands:
import { REDIS_COMMAND_INFO } from '@periodic/arsenic';
Object.entries(REDIS_COMMAND_INFO).forEach(([cmd, info]) => {
console.log(`${cmd}: ${info.category} - ${info.docs}`);
});Example event with Redis metadata:
{
"type": "db.query",
"db": "redis",
"adapter": "ioredis",
"operation": "HGETALL",
"durationMs": 84,
"slow": true,
"signals": ["slow_query"],
"severity": "warning",
"metadata": {
"command": "HGETALL",
"commandCategory": "slow",
"commandDocs": "https://periodic.dev/redis/hgetall",
"args": ["user:123"]
},
"timestamp": "2024-02-13T10:30:00.000Z"
}Command Categories Reference
| Command | Category | Description |
|---------|----------|-------------|
| KEYS | dangerous | Never use in production — blocks server while scanning all keys |
| FLUSHALL | dangerous | Deletes all keys in all databases |
| FLUSHDB | dangerous | Deletes all keys in current database |
| BLPOP | blocking | Blocking list pop operation |
| BRPOP | blocking | Blocking reverse list pop operation |
| BRPOPLPUSH | blocking | Blocking pop and push between lists |
| BLMOVE | blocking | Blocking atomic list move |
| HGETALL | slow | Can be slow with large hashes |
| SMEMBERS | slow | Returns all members — unbounded on large sets |
| LRANGE | slow | Scans a range of list elements |
| SORT | slow | Sorts list/set/sorted set — complex and memory-intensive |
| SCAN | slow | Better than KEYS but still monitored |
| SSCAN | slow | Iterates set members |
| HSCAN | slow | Iterates hash fields |
| ZSCAN | slow | Iterates sorted set members |
| SUNION | slow | Union of multiple sets |
| SINTER | slow | Intersection of multiple sets |
| SDIFF | slow | Difference between sets |
| SUNIONSTORE | slow | Union stored to destination |
| SINTERSTORE | slow | Intersection stored to destination |
| SDIFFSTORE | slow | Difference stored to destination |
| ZRANGE | slow | Range query on sorted set |
| ZRANGEBYSCORE | slow | Score-range query on sorted set |
| ZRANGEBYLEX | slow | Lex-range query on sorted set |
| ZREVRANGE | slow | Reverse range on sorted set |
| ZREVRANGEBYSCORE | slow | Reverse score-range query |
| ZINTERSTORE | slow | Intersection of sorted sets |
| ZUNIONSTORE | slow | Union of sorted sets |
| OBJECT | slow | Inspects object encoding/idle time |
| WAIT | slow | Blocks until replicas acknowledge writes |
📊 Multiple Exporters
Send events to multiple destinations:
const exporters = [
(event) => console.log(event),
async (event) => await sendToDatadog(event),
async (event) => await saveToDB(event),
];
const monitor = createMonitor({
exporter: async (event) => {
await Promise.allSettled(exporters.map((ex) => ex(event)));
},
});📚 Common Patterns
1. Express + MongoDB
import express from 'express';
import mongoose from 'mongoose';
import { createMonitor, expressContext, mongooseAdapter } from '@periodic/arsenic';
const app = express();
const monitor = createMonitor({
slowQueryThresholdMs: 200,
exporter: (event) => {
if (event.severity === 'critical') sendToPagerDuty(event);
},
});
app.use(expressContext(monitor));
mongooseAdapter(monitor, mongoose);
app.listen(3000);2. Express + PostgreSQL
import express from 'express';
import { Pool } from 'pg';
import { createMonitor, expressContext, pgAdapter } from '@periodic/arsenic';
const app = express();
const pool = new Pool();
const monitor = createMonitor({
slowQueryThresholdMs: 150,
exporter: (event) => {
if (event.severity === 'critical') sendToSlack(event);
},
});
app.use(expressContext(monitor));
pgAdapter(monitor, pool);
app.listen(3000);3. Fastify Integration
import Fastify from 'fastify';
import { createMonitor, fastifyContext, prismaAdapter } from '@periodic/arsenic';
const app = Fastify();
const monitor = createMonitor({
slowQueryThresholdMs: 200,
exporter: (event) => logger.info(event),
});
app.register(fastifyContext(monitor, {
attachUser: (req) => req.user?.id,
}));
prismaAdapter(monitor, prisma);
app.listen({ port: 3000 });4. OpenTelemetry Exporter
import { createMonitor, createOtelExporter } from '@periodic/arsenic';
const monitor = createMonitor({
exporter: createOtelExporter({
serviceName: 'my-service',
exportAsSpans: true,
exportAsMetrics: true,
}),
});5. Severity-Based Alerting
import { SignalSeverity } from '@periodic/arsenic';
const monitor = createMonitor({
exporter: (event) => {
switch (event.severity) {
case SignalSeverity.CRITICAL:
sendToPagerDuty(event);
break;
case SignalSeverity.WARNING:
sendToSlack(event);
break;
case SignalSeverity.INFO:
logger.info(event);
break;
}
},
});6. Custom Thresholds per Database
const pgMonitor = createMonitor({
slowQueryThresholdMs: 100, // Stricter for SQL
exporter: pgExporter,
});
const mongoMonitor = createMonitor({
slowQueryThresholdMs: 300, // Looser for MongoDB
exporter: mongoExporter,
});
pgAdapter(pgMonitor, pool);
mongooseAdapter(mongoMonitor, mongoose);7. Structured Logging Integration
import { createLogger, ConsoleTransport, JsonFormatter } from '@periodic/iridium';
const logger = createLogger({
transports: [new ConsoleTransport({ formatter: new JsonFormatter() })],
});
const monitor = createMonitor({
exporter: (event) => logger.info('arsenic.query', event),
});8. Production Configuration
import { createMonitor, createOtelExporter, SignalSeverity } from '@periodic/arsenic';
const isDevelopment = process.env.NODE_ENV === 'development';
const monitor = createMonitor({
slowQueryThresholdMs: isDevelopment ? 500 : 200,
emitPositiveSignals: false,
includeDocs: true,
exporter: async (event) => {
// Always log everything structured
logger.info('db.event', event);
// Alert on critical in production
if (!isDevelopment && event.severity === SignalSeverity.CRITICAL) {
await sendToPagerDuty(event);
}
// Warn on warning in all environments
if (event.severity === SignalSeverity.WARNING) {
await sendToSlack(event);
}
},
});
export default monitor;🎛️ Configuration Options
Monitor Configuration
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| slowQueryThresholdMs | number | 200 | Threshold in ms for slow query detection |
| exporter | Exporter | — | Required. Receives all emitted events |
| emitPositiveSignals | boolean | false | Enable INFO-level signals |
| includeDocs | boolean | true | Include explanations in events |
const monitor = createMonitor({
slowQueryThresholdMs: 200,
emitPositiveSignals: false,
includeDocs: true,
exporter: (event) => console.log(event),
});Framework Adapter Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| attachUser | (req) => string \| undefined | — | Extract user ID from request |
app.use(expressContext(monitor, {
attachUser: (req) => req.user?.id,
}));📋 API Reference
createMonitor(config)
Create a new monitor instance.
function createMonitor(config: MonitorConfig): MonitorFramework Adapters
expressContext(monitor: Monitor, options?: { attachUser?: (req) => string | undefined }): RequestHandler
fastifyContext(monitor: Monitor, options?: { attachUser?: (req) => string | undefined }): FastifyPluginDatabase Adapters
mongooseAdapter(monitor: Monitor, mongoose: Mongoose): void
prismaAdapter(monitor: Monitor, prisma: PrismaClient): void
pgAdapter(monitor: Monitor, pool: Pool): void
redisAdapter(monitor: Monitor, client: Redis | RedisClient): voidRedis Utilities
// Get categorization info for any Redis command
getRedisCommandInfo(command: string): { command: string; category: 'dangerous' | 'blocking' | 'slow' | 'normal'; docs: string }
// Array of command names classified as slow
SLOW_REDIS_COMMANDS: string[]
// Full map of all monitored Redis commands with metadata
REDIS_COMMAND_INFO: Record<string, { category: string; docs: string }>Exporters
createOtelExporter(options: {
serviceName: string;
exportAsSpans?: boolean;
exportAsMetrics?: boolean;
}): ExporterEvent Structure
interface ForgeEvent {
type: string;
db: string;
adapter: string;
model: string;
operation: string;
durationMs: number;
slow: boolean;
signals: ForgeSignal[];
severity: SignalSeverity;
explanations: Record<string, SignalExplanation>;
request?: RequestContext;
callsite?: Callsite;
metadata?: QueryMetadata;
timestamp: string;
}Supported Databases
| Database | Adapter | Status |
|----------|---------|--------|
| MongoDB | mongooseAdapter | ✅ Full support |
| PostgreSQL | prismaAdapter or pgAdapter | ✅ Full support |
| MySQL | prismaAdapter | ✅ Full support |
| SQLite | prismaAdapter | ✅ Full support |
| CockroachDB | prismaAdapter | ✅ Full support |
| Redis | redisAdapter | ✅ Full support + Command Monitoring |
🧩 Architecture
@periodic/arsenic/
├── src/
│ ├── core/ # Framework-agnostic core
│ │ ├── types.ts # TypeScript interfaces
│ │ ├── monitor.ts # Main Monitor class
│ │ ├── signals.ts # Signal detection engine
│ │ ├── severity.ts # Severity classification
│ │ └── context.ts # Request correlation
│ ├── adapters/ # Database adapter implementations
│ │ ├── mongoose.ts # Mongoose adapter
│ │ ├── prisma.ts # Prisma adapter
│ │ ├── pg.ts # PostgreSQL (pg) adapter
│ │ └── redis.ts # Redis adapter + command monitoring
│ ├── frameworks/ # Framework middleware
│ │ ├── express.ts # Express context middleware
│ │ └── fastify.ts # Fastify context plugin
│ ├── exporters/ # Exporter implementations
│ │ └── otel.ts # OpenTelemetry exporter
│ └── index.ts # Public APIDesign Philosophy:
- Core is pure TypeScript with no dependencies
- Adapters hook into database drivers cleanly without monkey-patching
- Frameworks attach request context via AsyncLocalStorage
- Exporters are just functions — bring your own destination
- Easy to extend with custom adapters and exporters
📈 Performance
Arsenic is optimized for production workloads:
- Zero blocking — Exporters run asynchronously, never delay responses
- AsyncLocalStorage — Lightweight, native request context propagation
- No monkey-patching — Clean hooks only, no prototype mutation
- Memory efficient — History cleanup prevents unbounded growth
- Exporter isolation — Exporter errors never crash your application
🚫 Explicit Non-Goals
This package intentionally does not include:
❌ Built-in dashboards (use Grafana, Datadog, etc.)
❌ Automatic query rewriting or fixing
❌ Vendor-specific lock-in of any kind
❌ Blocking behavior in production
❌ Magic or implicit behavior on import
❌ File or database transports (bring your own exporter)
❌ Metrics collection (use Prometheus, StatsD, etc.)
❌ Configuration files (configure in code)
Focus on doing one thing well: semantic runtime observability.
🎨 TypeScript Support
Full TypeScript support with complete type safety:
import type {
ForgeEvent,
ForgeSignal,
SignalSeverity,
Exporter,
MonitorConfig,
QueryMetadata,
RequestContext,
} from '@periodic/arsenic';
const monitor = createMonitor({
slowQueryThresholdMs: 200,
exporter: (event: ForgeEvent) => {
const severity: SignalSeverity = event.severity;
const signals: ForgeSignal[] = event.signals;
console.log({ severity, signals });
},
});🤝 Related Packages
Part of the Periodic series by Uday Thakur:
- @periodic/iridium - Structured logging
- @periodic/obsidian - HTTP error handling
- @periodic/titanium - Rate limiting
- @periodic/osmium - Redis caching
Build complete, production-ready APIs with the Periodic series!
📖 Documentation
🧪 Testing
# Run tests
npm test
# Run tests with coverage
npm run test:coverage
# Run tests in watch mode
npm run test:watchNote: All tests achieve >80% code coverage.
🛠️ Production Recommendations
Environment Variables
SLOW_QUERY_THRESHOLD_MS=200
NODE_ENV=production
APP_VERSION=1.0.0Log Aggregation
Pair with @periodic/iridium for structured JSON output:
import { createLogger, ConsoleTransport, JsonFormatter } from '@periodic/iridium';
import { createMonitor } from '@periodic/arsenic';
const logger = createLogger({
transports: [new ConsoleTransport({ formatter: new JsonFormatter() })],
});
const monitor = createMonitor({
exporter: (event) => logger.info('db.event', event),
});
// Pipe to Elasticsearch, Datadog, CloudWatch, etc.Error Monitoring
Integrate with error tracking:
const monitor = createMonitor({
exporter: (event) => {
if (event.severity === 'critical') {
Sentry.captureEvent({ message: event.signals.join(', '), extra: event });
}
},
});📝 License
MIT © Uday Thakur
🙏 Contributing
Contributions are welcome! Please read CONTRIBUTING.md for details on:
- Code of conduct
- Development setup
- Pull request process
- Coding standards
- Architecture principles
📞 Support
- 📧 Email: [email protected]
- 🐛 Issues: GitHub Issues
- 💬 Discussions: GitHub Discussions
🌟 Show Your Support
Give a ⭐️ if this project helped you build better applications!
Built with ❤️ by Uday Thakur for production-grade Node.js applications
