Tally Guide for Developers

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.

Table of Contents

Conceptual Overview

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.

Metric Types

Exporters

Exporters push metrics to an external system. The built‑in exporters are:

Setup & Installation

Follow these steps to get Tally running in a fresh Node project. The commands work on Windows, macOS, and Linux.

1. Create a project

mkdir my-tally-app && cd my-tally-app
npm init -y

2. Install the SDK

npm install @tally/sdk

The package size is 18 KB (gzipped) and adds no transitive dependencies.

3. Add a TypeScript config (optional but recommended)

npx tsc --init --strict

Strict mode ensures the type safety Tally relies on.

4. Write your first metric definition

// 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: []
});

5. Initialize a singleton Tally instance

// tally.ts
import { Tally } from '@tally/sdk';
import { PrometheusExporter } from '@tally/sdk/exporters';

export const tally = new Tally({
  exporter: new PrometheusExporter({ port: 9464 })
});

6. Wire it into your app

// 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.

Core Workflows

Once the basics are in place, you will use three main patterns: emitting counters, recording timers, and publishing gauges.

Incrementing a Counter

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.

Recording a Timer

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.

Setting a Gauge

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.

Batch Export

If you need to flush metrics on demand (e.g., before a graceful shutdown), call:

await tally.flush(); // returns a Promise

Advanced Patterns

The following patterns help you scale Tally in larger services.

Dynamic Labels with Type Safety

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.

Hierarchical Timers

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.

Multiple Exporters Simultaneously

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 Comparison Table

FeaturePrometheus ExporterJSON ExporterStatsD Exporter
TransportHTTP GET /metricsWritable stream (stdout, file, socket)UDP packets
Payload Size~2 KB per scrape1 line per metric (~150 B)~200 B per packet
Latency Impactnegligible (scrape‑only)immediate writefire‑and‑forget
Supported TypesCounters, Gauges, HistogramsAll types as JSONCounters, Gauges
Best Use‑CaseProduction monitoringDebugging / log aggregationExisting StatsD pipelines

Using Tally with Serverless

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.

Common Mistakes

Even experienced engineers slip up. Below are the five most frequent errors and how to fix them.

1. Defining Metrics Inside Functions

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.

2. Forgetting to Register Labels

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.

3. Creating a New Tally Instance Per Request

Each Tally holds its own registry. Spawning many instances prevents aggregation and overwhelms exporters. Use a singleton pattern (as shown in the setup section).

4. Mixing Units

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.

5. Ignoring Exporter Errors

Exporters return promises. If you fire‑and‑forget, network failures may go unnoticed. Wrap await tally.flush() in a try/catch during graceful shutdown.

FAQ

What is Tally and why should developers use it?

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.

How do I install Tally in a Node project?

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.

Can Tally send data to multiple back‑ends?

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.

What are the most common mistakes when using Tally?

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.

Where can I find advanced pattern examples?

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.

Conclusion

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.

Get tools like this in your inbox
One useful tool per week. No spam. Unsubscribe anytime.