File: metrics-monitoring-and-alerting/metrics.py

Date: 2026-06-05

Time: 13:52

metrics-monitoring-and-alerting/metrics.py

Purpose

This file implements an in-memory time-series metrics monitoring and alerting system — the kind of thing you'd build if asked "Design a metrics monitoring system like Datadog or Prometheus" in a system design interview. It owns the entire pipeline: ingestion of metric data points, time-windowed querying with aggregation, alert rule evaluation with state machines, and data lifecycle management (downsampling + retention).

Key Components

Data Classes

MetricsService

The core class. Single-node, in-memory — no persistence or distribution, consistent with the SDI interview scope of demonstrating architecture rather than production readiness.

Storage model: data is a dict keyed by (metricname, frozenset(tags.items())). Values are sorted lists of (timestamp, value) tuples. The frozenset key means each unique tag combination gets its own series — this is the standard time-series cardinality model (identical to how Prometheus stores series by label set).

Ingestion:

Querying:

Alerting:

Data Lifecycle:

Patterns

Sorted-list time series with bisect. Every series is kept sorted by timestamp, enabling O(log n) range queries via bisectleft/bisectright. This is the in-memory analog of a time-series database's sorted index. The tuple comparison (timestamp,) works because Python compares tuples element-by-element.

Tag-based series multiplexing. Using frozenset(tags.items()) as part of the key gives each unique label set its own series, then matchingkeys does a linear scan with subset matching for queries. This mirrors how Prometheus/Datadog handle label cardinality — each unique label combination is a distinct time series.

Alert state machine. Four states: OK → PENDING → ALERTING → RESOLVED → (back to OK or PENDING). The PENDING state implements the duration_seconds "for" clause — the condition must persist for the specified duration before firing. This prevents noisy alerts from transient spikes.

Observer pattern for alert callbacks. callbacks list with onalert() registration. Simple pub-sub for notification delivery.

Dependencies

Imports: Only stdlib — bisect for sorted-list operations, math for percentile floor/ceil, dataclasses for data modeling. No external dependencies.

Imported by: testmetrics.py and testsmoke.py — the test suites.

Flow

Ingestion Path

DataPointingest() → compute (metric, frozenset(tags)) key → bisect.bisect_right to find insert position → list.insert in sorted order.

Query Path

query()matchingkeys() scans data for matching metric + tag subset → slice() each matching series → merge + sort across series → bucketize() into time windows → aggregate() each bucket → return list of {timestamp, value} dicts.

Alert Evaluation Path

evaluatealerts(currenttime) → for each rule: checkcondition() gathers data in the lookback window → computes avg (or rate for rate_change) → compare to threshold → drive state machine → emit Alert objects for transitions → invoke callbacks.

Data Lifecycle Path

downsample(current_time) → for each series, apply age-based bucketing rules → replace raw points with averaged buckets.

applyretention(currenttime) → bisect to find cutoff index → truncate series → delete empty keys.

Invariants

1. Series are always sorted by timestamp. ingest() maintains this via bisectright insertion. downsample() and applyretention() preserve it (re-sort after downsample, bisect-based truncation for retention).

2. Alert state transitions follow the state machine. OK → PENDING → ALERTING is the only firing path. PENDING resets to OK (not RESOLVED) if the condition clears before duration_seconds elapses. Only ALERTING → RESOLVED produces a RESOLVED alert.

3. Tag matching is a subset check. matchingkeys requires all tagsfilter key-value pairs to be present in the series tags, but extra tags in the series are fine. This means tagsfilter={"host": "web-01"} matches series with tags {"host": "web-01", "region": "us-east"}.

4. checkcondition uses the average of all points in the lookback window for threshold comparisons (gt/lt/etc.), not the latest point. The window is max(duration_seconds, 60).

5. Downsampling is lossy. It replaces raw points with bucket averages — min/max/percentile accuracy is lost for downsampled data.

Error Handling

Essentially none — this is interview-demonstration code. No validation on ingestion (negative timestamps, NaN values), no bounds checking on aggregation types, no protection against tag cardinality explosion. aggregate silently falls back to avg for unrecognized aggregation names. check_condition returns (False, 0) for empty data or unrecognized conditions, silently suppressing evaluation.

Topics to Explore

Beliefs