diff --git a/pyproject.toml b/pyproject.toml index a282ecf1..edb94ddd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,6 @@ dependencies = [ "aiohttp>=3.11.0,<4", "cryptography>=46.0.0", "numpy>=2.0.0,<3", - "prometheus-client>=0.21.0,<1", "aioquic>=1.2.0,<2", "pyyaml>=6.0.0,<7", ] diff --git a/src/lean_spec/subspecs/api/endpoints/__init__.py b/src/lean_spec/subspecs/api/endpoints/__init__.py index f06768a9..e2c0f12f 100644 --- a/src/lean_spec/subspecs/api/endpoints/__init__.py +++ b/src/lean_spec/subspecs/api/endpoints/__init__.py @@ -1,10 +1,9 @@ """API endpoint specifications.""" -from . import checkpoints, health, metrics, states +from . import checkpoints, health, states __all__ = [ "checkpoints", "health", - "metrics", "states", ] diff --git a/src/lean_spec/subspecs/api/endpoints/metrics.py b/src/lean_spec/subspecs/api/endpoints/metrics.py deleted file mode 100644 index 87417dd4..00000000 --- a/src/lean_spec/subspecs/api/endpoints/metrics.py +++ /dev/null @@ -1,27 +0,0 @@ -"""[OPTIONAL] Metrics endpoint specification and handler.""" - -from aiohttp import web - -from lean_spec.subspecs.metrics import generate_metrics - -CHARSET = "utf-8" -"""Character encoding for Prometheus metrics.""" - - -async def handle(_request: web.Request) -> web.Response: - """ - Handle metrics request. - - Returns Prometheus-format metrics. Implementation-specific; not required - for conformance. - - Response: Prometheus text format (text/plain; version=0.0.4) - - Status Codes: - 200 OK: Metrics returned. - """ - return web.Response( - body=generate_metrics(), - content_type="text/plain; version=0.0.4", - charset=CHARSET, - ) diff --git a/src/lean_spec/subspecs/api/routes.py b/src/lean_spec/subspecs/api/routes.py index 2f0b45c7..8565ddff 100644 --- a/src/lean_spec/subspecs/api/routes.py +++ b/src/lean_spec/subspecs/api/routes.py @@ -4,12 +4,11 @@ from aiohttp import web -from .endpoints import checkpoints, health, metrics, states +from .endpoints import checkpoints, health, states ROUTES: dict[str, Callable[[web.Request], Awaitable[web.Response]]] = { "/lean/v0/health": health.handle, "/lean/v0/states/finalized": states.handle_finalized, "/lean/v0/checkpoints/justified": checkpoints.handle_justified, - "/metrics": metrics.handle, } """All API routes mapped to their handlers.""" diff --git a/src/lean_spec/subspecs/metrics/__init__.py b/src/lean_spec/subspecs/metrics/__init__.py deleted file mode 100644 index 36803521..00000000 --- a/src/lean_spec/subspecs/metrics/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -Metrics module for observability. - -Provides counters, gauges, and histograms for tracking consensus client behavior. -Exposes metrics in Prometheus text format. -""" - -from .registry import ( - REGISTRY, - attestations_produced, - block_processing_time, - blocks_processed, - blocks_proposed, - finalized_slot, - generate_metrics, - head_slot, - justified_slot, - validators_count, -) - -__all__ = [ - "REGISTRY", - "attestations_produced", - "block_processing_time", - "blocks_processed", - "blocks_proposed", - "finalized_slot", - "generate_metrics", - "head_slot", - "justified_slot", - "validators_count", -] diff --git a/src/lean_spec/subspecs/metrics/registry.py b/src/lean_spec/subspecs/metrics/registry.py deleted file mode 100644 index 670574a0..00000000 --- a/src/lean_spec/subspecs/metrics/registry.py +++ /dev/null @@ -1,80 +0,0 @@ -""" -Metric registry using prometheus_client. - -Provides pre-defined metrics for a consensus client. -Exposes metrics in Prometheus text format via the /metrics endpoint. -""" - -from __future__ import annotations - -from prometheus_client import ( - CollectorRegistry, - Counter, - Gauge, - Histogram, - generate_latest, -) - -# Create a dedicated registry for lean-spec metrics. -# -# Using a dedicated registry avoids pollution from default Python process metrics. -REGISTRY = CollectorRegistry() - -head_slot = Gauge( - "lean_head_slot", - "Current head slot", - registry=REGISTRY, -) - -justified_slot = Gauge( - "lean_justified_slot", - "Latest justified slot", - registry=REGISTRY, -) - -finalized_slot = Gauge( - "lean_finalized_slot", - "Latest finalized slot", - registry=REGISTRY, -) - -validators_count = Gauge( - "lean_validators_count", - "Active validators", - registry=REGISTRY, -) - -blocks_processed = Counter( - "lean_blocks_processed_total", - "Total blocks processed", - registry=REGISTRY, -) - -block_processing_time = Histogram( - "lean_block_processing_seconds", - "Block processing duration", - buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5), - registry=REGISTRY, -) - -blocks_proposed = Counter( - "lean_blocks_proposed_total", - "Blocks proposed by this node", - registry=REGISTRY, -) - -attestations_produced = Counter( - "lean_attestations_produced_total", - "Attestations produced by this node", - registry=REGISTRY, -) - - -def generate_metrics() -> bytes: - """ - Generate Prometheus metrics output. - - Returns: - Prometheus text format output as bytes. - """ - return generate_latest(REGISTRY) diff --git a/src/lean_spec/subspecs/sync/service.py b/src/lean_spec/subspecs/sync/service.py index db7e7e5e..6311c1fc 100644 --- a/src/lean_spec/subspecs/sync/service.py +++ b/src/lean_spec/subspecs/sync/service.py @@ -42,7 +42,6 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any -from lean_spec.subspecs import metrics from lean_spec.subspecs.chain.clock import SlotClock from lean_spec.subspecs.containers import ( Block, @@ -241,24 +240,12 @@ def _process_block_wrapper( # Delegate to the actual block processor. # # The processor validates the block and updates forkchoice state. - with metrics.block_processing_time.time(): - new_store = self.process_block(store, block) + new_store = self.process_block(store, block) - # Track metrics after successful processing. + # Track processed blocks. # # We only count blocks that pass validation and update the store. self._blocks_processed += 1 - metrics.blocks_processed.inc() - - # Update chain state metrics. - metrics.head_slot.set(float(new_store.blocks[new_store.head].slot)) - metrics.justified_slot.set(float(new_store.latest_justified.slot)) - metrics.finalized_slot.set(float(new_store.latest_finalized.slot)) - - # Update validator count from head state. - head_state = new_store.states.get(new_store.head) - if head_state is not None: - metrics.validators_count.set(float(len(head_state.validators))) # Persist block and state to database if available. # diff --git a/src/lean_spec/subspecs/validator/service.py b/src/lean_spec/subspecs/validator/service.py index 63364423..e077bb4e 100644 --- a/src/lean_spec/subspecs/validator/service.py +++ b/src/lean_spec/subspecs/validator/service.py @@ -37,7 +37,6 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, cast -from lean_spec.subspecs import metrics from lean_spec.subspecs.chain.clock import Interval, SlotClock from lean_spec.subspecs.chain.config import ATTESTATION_COMMITTEE_COUNT from lean_spec.subspecs.containers import ( @@ -297,7 +296,6 @@ async def _maybe_produce_block(self, slot: Slot) -> None: self._store_proposer_attestation_signature(signed_block, validator_index) self._blocks_produced += 1 - metrics.blocks_proposed.inc() # Emit the block for network propagation. await self.on_block(signed_block) @@ -371,7 +369,6 @@ async def _produce_attestations(self, slot: Slot) -> None: signed_attestation = self._sign_attestation(attestation_data, validator_index) self._attestations_produced += 1 - metrics.attestations_produced.inc() # Process attestation locally before publishing. # diff --git a/tests/lean_spec/subspecs/metrics/test_registry.py b/tests/lean_spec/subspecs/metrics/test_registry.py deleted file mode 100644 index ea151354..00000000 --- a/tests/lean_spec/subspecs/metrics/test_registry.py +++ /dev/null @@ -1,143 +0,0 @@ -"""Tests for the Prometheus metrics registry.""" - -from __future__ import annotations - -import time - -from prometheus_client import REGISTRY as DEFAULT_REGISTRY - -from lean_spec.subspecs.metrics import ( - REGISTRY, - attestations_produced, - block_processing_time, - blocks_processed, - blocks_proposed, - finalized_slot, - generate_metrics, - head_slot, - justified_slot, - validators_count, -) - - -class TestMetricTypes: - """Tests for metric type behavior.""" - - def test_counter_increments_correctly(self) -> None: - """Counter metrics increment by one on each call.""" - initial = blocks_processed._value.get() - blocks_processed.inc() - assert blocks_processed._value.get() == initial + 1.0 - - def test_gauge_sets_value_correctly(self) -> None: - """Gauge metrics can be set to arbitrary values.""" - head_slot.set(42.0) - assert head_slot._value.get() == 42.0 - - head_slot.set(100.0) - assert head_slot._value.get() == 100.0 - - def test_histogram_observes_values(self) -> None: - """Histogram metrics record observations.""" - # Get initial sample count from the histogram - initial_samples = list(block_processing_time.collect())[0].samples - initial_count = next(s.value for s in initial_samples if s.name.endswith("_count")) - - block_processing_time.observe(0.05) - - # Verify count increased - new_samples = list(block_processing_time.collect())[0].samples - new_count = next(s.value for s in new_samples if s.name.endswith("_count")) - assert new_count == initial_count + 1 - - -class TestMetricDefinitions: - """Tests for pre-defined metric definitions.""" - - def test_node_information_gauges_exist(self) -> None: - """Node information gauges are defined.""" - assert head_slot is not None - assert justified_slot is not None - assert finalized_slot is not None - assert validators_count is not None - - def test_block_processing_metrics_exist(self) -> None: - """Block processing metrics are defined.""" - assert blocks_processed is not None - assert block_processing_time is not None - - def test_validator_production_metrics_exist(self) -> None: - """Validator production metrics are defined.""" - assert blocks_proposed is not None - assert attestations_produced is not None - - -class TestPrometheusOutput: - """Tests for Prometheus text format output.""" - - def test_generate_metrics_returns_bytes(self) -> None: - """Generate metrics returns bytes in Prometheus format.""" - output = generate_metrics() - assert isinstance(output, bytes) - - def test_output_contains_metric_names(self) -> None: - """Output contains expected metric names.""" - output = generate_metrics().decode("utf-8") - - assert "lean_head_slot" in output - assert "lean_blocks_processed_total" in output - assert "lean_block_processing_seconds" in output - - def test_output_contains_help_text(self) -> None: - """Output contains HELP lines for metrics.""" - output = generate_metrics().decode("utf-8") - - assert "# HELP lean_head_slot" in output - assert "# TYPE lean_head_slot gauge" in output - - def test_output_contains_histogram_buckets(self) -> None: - """Output contains histogram bucket values.""" - output = generate_metrics().decode("utf-8") - - # Histogram exports include _bucket, _count, _sum - assert "lean_block_processing_seconds_bucket" in output - assert "lean_block_processing_seconds_count" in output - assert "lean_block_processing_seconds_sum" in output - - -class TestRegistryIsolation: - """Tests for registry isolation from default metrics.""" - - def test_registry_is_dedicated(self) -> None: - """Our registry is separate from default prometheus registry.""" - assert REGISTRY is not DEFAULT_REGISTRY - - def test_metrics_registered_to_custom_registry(self) -> None: - """All metrics are registered to our custom registry.""" - # Verify a metric is in our registry by generating output - output = generate_metrics().decode("utf-8") - - # If head_slot is in our registry, it should appear in output - assert "lean_head_slot" in output - - -class TestHistogramTiming: - """Tests for histogram timing context manager.""" - - def test_time_context_manager_records_duration(self) -> None: - """Histogram time() context manager records duration.""" - # Get initial values from samples - initial_samples = list(block_processing_time.collect())[0].samples - initial_count = next(s.value for s in initial_samples if s.name.endswith("_count")) - initial_sum = next(s.value for s in initial_samples if s.name.endswith("_sum")) - - with block_processing_time.time(): - time.sleep(0.01) - - # Get new values - new_samples = list(block_processing_time.collect())[0].samples - new_count = next(s.value for s in new_samples if s.name.endswith("_count")) - new_sum = next(s.value for s in new_samples if s.name.endswith("_sum")) - - assert new_count == initial_count + 1 - assert new_sum > initial_sum diff --git a/uv.lock b/uv.lock index 50998e14..84fc8429 100644 --- a/uv.lock +++ b/uv.lock @@ -880,7 +880,6 @@ dependencies = [ { name = "httpx" }, { name = "lean-multisig-py" }, { name = "numpy" }, - { name = "prometheus-client" }, { name = "pydantic" }, { name = "pyyaml" }, { name = "typing-extensions" }, @@ -941,7 +940,6 @@ requires-dist = [ { name = "httpx", specifier = ">=0.28.0,<1" }, { name = "lean-multisig-py", git = "https://github.com/anshalshukla/leanMultisig-py?branch=devnet2" }, { name = "numpy", specifier = ">=2.0.0,<3" }, - { name = "prometheus-client", specifier = ">=0.21.0,<1" }, { name = "pydantic", specifier = ">=2.12.0,<3" }, { name = "pyyaml", specifier = ">=6.0.0,<7" }, { name = "typing-extensions", specifier = ">=4.4" }, @@ -1516,15 +1514,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] -[[package]] -name = "prometheus-client" -version = "0.24.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/58/a794d23feb6b00fc0c72787d7e87d872a6730dd9ed7c7b3e954637d8f280/prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9", size = 85616, upload-time = "2026-01-14T15:26:26.965Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055", size = 64057, upload-time = "2026-01-14T15:26:24.42Z" }, -] - [[package]] name = "prompt-toolkit" version = "3.0.52"