Tally is a type‑safe analytics library designed for JavaScript and TypeScript developers. It gives you compile‑time guarantees, a tiny runtime footprint, and seamless integration with existing logging pipelines. This guide walks you through the core concepts, step‑by‑step setup, essential workflows, advanced patterns, and the most common mistakes to avoid.
Tally treats metrics as first‑class TypeScript types. You declare a metric schema, instantiate a Tally object, and then emit values that match the schema. The library validates at compile time, so you cannot accidentally send a string where a number is expected.
Exporters push metrics to an external system. The built‑in exporters are:
PrometheusExporter – exposes a /metrics endpoint.JsonExporter – writes line‑delimited JSON to a file or stream.StatsDExporter – sends UDP packets compatible with Datadog, Graphite, etc.Follow these steps to get Tally running in a fresh Node project. The commands work on Windows, macOS, and Linux.
mkdir my-tally-app && cd my-tally-app
npm init -y
npm install @tally/sdk
The package size is 18 KB (gzipped) and adds no transitive dependencies.
npx tsc --init --strict
Strict mode ensures the type safety Tally relies on.
// metrics.ts
import { Counter, Gauge, Histogram } from '@tally/sdk';
export const requests = Counter.define('requests_total', {
description: 'Total number of HTTP requests',
labels: ['method', 'status']
});
export const latency = Histogram.define('request_latency_seconds', {
description: 'Request latency distribution',
buckets: [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
labels: ['method']
});
export const cpuLoad = Gauge.define('cpu_load_percent', {
description: 'Current CPU load',
labels: []
});
// tally.ts
import { Tally } from '@tally/sdk';
import { PrometheusExporter } from '@tally/sdk/exporters';
export const tally = new Tally({
exporter: new PrometheusExporter({ port: 9464 })
});
// server.ts
import express from 'express';
import { tally } from './tally';
import { requests, latency } from './metrics';
const app = express();
app.use((req, res, next) => {
const end = latency.startTimer({ method: req.method });
res.on('finish', () => {
requests.inc({ method: req.method, status: res.statusCode });
end();
});
next();
});
app.get('/', (req, res) => res.send('Hello Tally!'));
app.listen(3000, () => console.log('Server running on :3000'));
Now http://localhost:9464/metrics returns Prometheus‑compatible output.
Once the basics are in place, you will use three main patterns: emitting counters, recording timers, and publishing gauges.
import { requests } from './metrics';
requests.inc({ method: 'GET', status: 200 });
requests.inc({ method: 'POST', status: 201 }, 3); // add 3 at once
Counters cannot be decremented. Attempting to call dec() will raise a compile‑time error.
import { latency } from './metrics';
const stop = latency.startTimer({ method: 'GET' });
// … do work …
stop(); // automatically records elapsed seconds
The timer uses process.hrtime.bigint() under the hood, giving nanosecond precision.
import { cpuLoad } from './metrics';
setInterval(() => {
const load = os.loadavg()[0] * 100; // simple approximation
cpuLoad.set({}, load);
}, 5_000);
Gauges accept any numeric value, positive or negative.
If you need to flush metrics on demand (e.g., before a graceful shutdown), call:
await tally.flush(); // returns a Promise
The following patterns help you scale Tally in larger services.
Define a label union type and reuse it across metrics.
// labels.ts
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
export type HttpStatus = 200 | 201 | 400 | 401 | 403 | 404 | 500;
// metrics.ts
import { HttpMethod, HttpStatus } from './labels';
export const requests = Counter.define('requests_total', {
description: 'HTTP requests',
labels: ['method' as const, 'status' as const] // enforce literal types
});
Now requests.inc({ method: 'PATCH', status: 200 }) fails at compile time because 'PATCH' is not part of HttpMethod.
Wrap multiple timers to measure nested operations.
const outer = latency.startTimer({ method: 'GET' });
await db.query(...);
const inner = latency.startTimer({ method: 'DB' });
// … DB work …
inner();
outer();
The exported data will show two separate histograms, letting you spot bottlenecks.
import { PrometheusExporter, JsonExporter } from '@tally/sdk/exporters';
export const tally = new Tally({
exporter: [
new PrometheusExporter({ port: 9464 }),
new JsonExporter({ stream: process.stdout })
]
});
This configuration writes Prometheus text to a HTTP endpoint and streams JSON lines to stdout. Each exporter runs independently, so a failure in one does not affect the other.
| Feature | Prometheus Exporter | JSON Exporter | StatsD Exporter |
|---|---|---|---|
| Transport | HTTP GET /metrics | Writable stream (stdout, file, socket) | UDP packets |
| Payload Size | ~2 KB per scrape | 1 line per metric (~150 B) | ~200 B per packet |
| Latency Impact | negligible (scrape‑only) | immediate write | fire‑and‑forget |
| Supported Types | Counters, Gauges, Histograms | All types as JSON | Counters, Gauges |
| Best Use‑Case | Production monitoring | Debugging / log aggregation | Existing StatsD pipelines |
In AWS Lambda, create the Tally instance outside the handler so it reuses the same object across invocations:
// lambda.ts
import { tally } from './tally';
export const handler = async (event) => {
// emit metrics
requests.inc({ method: 'POST', status: 200 });
// no need to flush; the platform discards after the response
};
This reduces cold‑start overhead and keeps metric aggregation accurate.
Even experienced engineers slip up. Below are the five most frequent errors and how to fix them.
When you call Counter.define() inside a request handler, a new metric object is created on every call. This leads to memory bloat and duplicate series in Prometheus. Always define metrics in a module that is imported once.
If you emit a metric with a missing label, Tally throws a runtime error. Use TypeScript's literal types for labels to catch mistakes early.
Each Tally holds its own registry. Spawning many instances prevents aggregation and overwhelms exporters. Use a singleton pattern (as shown in the setup section).
Histograms for latency should always use seconds. Some developers record milliseconds, which breaks downstream dashboards. The library does not convert units automatically, so be consistent.
Exporters return promises. If you fire‑and‑forget, network failures may go unnoticed. Wrap await tally.flush() in a try/catch during graceful shutdown.
Tally is a lightweight, type‑safe analytics library for JavaScript/TypeScript. It lets you define metrics in code, emit them at runtime, and ship them to any collector without runtime reflection. Developers choose Tally for its zero‑dependency core, compile‑time guarantees and easy integration with existing logging pipelines.
Run npm install @tally/sdk or yarn add @tally/sdk. The package is about 18 KB gzipped and works with Node 14+ and modern browsers.
Yes. You can configure multiple exporters – for example, a Prometheus exporter for metrics and a JSON exporter for event logs. Each exporter implements the same Exporter interface, so swapping or adding one is a single line change.
Developers often forget to register metric definitions before use, or they emit untyped values that break compile‑time safety. Another frequent error is creating a new Tally instance per request, which defeats aggregation. Use a singleton pattern instead.
The official repo includes an examples/ folder with patterns such as request‑level tagging, dynamic dimension handling, and hierarchical timers. The README also links to a community blog series that dives deeper.
Ready to start measuring? Follow the steps above, integrate the singleton tally into your codebase, and watch your observability improve instantly.
Tally gives developers a fast, type‑safe way to collect metrics without adding heavy dependencies. By defining metrics once, using a singleton Tally instance, and choosing the right exporter, you get reliable data for monitoring and debugging. Avoid the common pitfalls listed, and explore the advanced patterns to get the most out of your telemetry stack.