-
Notifications
You must be signed in to change notification settings - Fork 33
feat: exporter prometheus_client /metrics (JEP-0013 Phase 2) #934
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ | |
|
|
||
| import logging | ||
| import os | ||
| import time | ||
| from abc import ABCMeta, abstractmethod | ||
| from contextlib import asynccontextmanager | ||
| from dataclasses import field | ||
|
|
@@ -113,11 +114,36 @@ def client(cls) -> str: | |
| def extra_labels(self) -> dict[str, str]: | ||
| return {} | ||
|
|
||
| def _record_operation_metrics( | ||
| self, | ||
| *, | ||
| operation: str, | ||
| result: str, | ||
| duration_seconds: float, | ||
| error_type: str | None = None, | ||
| ) -> None: | ||
| from jumpstarter.metrics.registry import ( | ||
| exemplars_from_log_context, | ||
| exporter_from_log_context, | ||
| get_registry, | ||
| ) | ||
|
|
||
| get_registry().record_operation( | ||
| exporter=exporter_from_log_context(default=self.name if hasattr(self, "name") else "unknown"), | ||
| operation=operation, | ||
| result=result, | ||
| driver_type=self.driver_type, | ||
| duration_seconds=duration_seconds, | ||
| exemplars=exemplars_from_log_context(), | ||
| error_type=error_type, | ||
| ) | ||
|
|
||
| async def DriverCall(self, request, context): | ||
| """ | ||
| :meta private: | ||
| """ | ||
| op = request.method | ||
| started = time.perf_counter() | ||
| self.logger.info( | ||
| "Operation started", | ||
| extra={"operation": op, "driver_type": self.driver_type}, | ||
|
Comment on lines
114
to
149
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The AI-generated, human reviewed |
||
|
|
@@ -132,6 +158,11 @@ async def DriverCall(self, request, context): | |
| else: | ||
| result = await to_thread.run_sync(method, *args) | ||
|
|
||
| self._record_operation_metrics( | ||
| operation=op, | ||
| result="success", | ||
| duration_seconds=time.perf_counter() - started, | ||
| ) | ||
| self.logger.info( | ||
| "Operation completed", | ||
| extra={"operation": op, "driver_type": self.driver_type, "result": "success"}, | ||
|
|
@@ -141,41 +172,77 @@ async def DriverCall(self, request, context): | |
| result=encode_value(result), | ||
| ) | ||
| except NotImplementedError as e: | ||
| self._record_operation_metrics( | ||
| operation=op, | ||
| result="failure", | ||
| duration_seconds=time.perf_counter() - started, | ||
| error_type="not_implemented", | ||
| ) | ||
| self.logger.warning( | ||
| "Operation failed", | ||
| extra={"operation": op, "driver_type": self.driver_type, | ||
| "result": "failure", "error_type": "not_implemented"}, | ||
| ) | ||
| await context.abort(StatusCode.UNIMPLEMENTED, str(e)) | ||
| except ValueError as e: | ||
| self._record_operation_metrics( | ||
| operation=op, | ||
| result="failure", | ||
| duration_seconds=time.perf_counter() - started, | ||
| error_type="validation_error", | ||
| ) | ||
| self.logger.warning( | ||
| "Operation failed", | ||
| extra={"operation": op, "driver_type": self.driver_type, | ||
| "result": "failure", "error_type": "validation_error"}, | ||
| ) | ||
| await context.abort(StatusCode.INVALID_ARGUMENT, str(e)) | ||
| except TimeoutError as e: | ||
| self._record_operation_metrics( | ||
| operation=op, | ||
| result="failure", | ||
| duration_seconds=time.perf_counter() - started, | ||
| error_type="timeout", | ||
| ) | ||
| self.logger.warning( | ||
| "Operation failed", | ||
| extra={"operation": op, "driver_type": self.driver_type, | ||
| "result": "failure", "error_type": "timeout"}, | ||
| ) | ||
| await context.abort(StatusCode.DEADLINE_EXCEEDED, str(e)) | ||
| except ConnectionError as e: | ||
| self._record_operation_metrics( | ||
| operation=op, | ||
| result="failure", | ||
| duration_seconds=time.perf_counter() - started, | ||
| error_type="connection_error", | ||
| ) | ||
| self.logger.warning( | ||
| "Operation failed", | ||
| extra={"operation": op, "driver_type": self.driver_type, | ||
| "result": "failure", "error_type": "connection_error"}, | ||
| ) | ||
| await context.abort(StatusCode.UNAVAILABLE, str(e)) | ||
| except OSError as e: | ||
| self._record_operation_metrics( | ||
| operation=op, | ||
| result="failure", | ||
| duration_seconds=time.perf_counter() - started, | ||
| error_type="device_error", | ||
| ) | ||
| self.logger.warning( | ||
| "Operation failed", | ||
| extra={"operation": op, "driver_type": self.driver_type, | ||
| "result": "failure", "error_type": "device_error"}, | ||
| ) | ||
| await context.abort(StatusCode.INTERNAL, str(e)) | ||
| except Exception as e: | ||
| self._record_operation_metrics( | ||
| operation=op, | ||
| result="failure", | ||
| duration_seconds=time.perf_counter() - started, | ||
| error_type="internal_error", | ||
| ) | ||
| self.logger.warning( | ||
| "Operation failed", | ||
| extra={"operation": op, "driver_type": self.driver_type, | ||
|
Comment on lines
239
to
248
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider adding
Comment on lines
172
to
248
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider adding test drivers whose |
||
|
|
@@ -188,6 +255,7 @@ async def StreamingDriverCall(self, request, context): | |
| :meta private: | ||
| """ | ||
| op = request.method | ||
| started = time.perf_counter() | ||
| self.logger.info( | ||
| "Operation started", | ||
| extra={"operation": op, "driver_type": self.driver_type}, | ||
|
|
@@ -209,46 +277,87 @@ async def StreamingDriverCall(self, request, context): | |
| uuid=str(uuid4()), | ||
| result=encode_value(result), | ||
| ) | ||
| self._record_operation_metrics( | ||
| operation=op, | ||
| result="success", | ||
| duration_seconds=time.perf_counter() - started, | ||
| ) | ||
| self.logger.info( | ||
| "Operation completed", | ||
| extra={"operation": op, "driver_type": self.driver_type, "result": "success"}, | ||
| ) | ||
| except NotImplementedError as e: | ||
| self._record_operation_metrics( | ||
| operation=op, | ||
| result="failure", | ||
| duration_seconds=time.perf_counter() - started, | ||
| error_type="not_implemented", | ||
| ) | ||
| self.logger.warning( | ||
| "Operation failed", | ||
| extra={"operation": op, "driver_type": self.driver_type, | ||
| "result": "failure", "error_type": "not_implemented"}, | ||
| ) | ||
| await context.abort(StatusCode.UNIMPLEMENTED, str(e)) | ||
| except ValueError as e: | ||
| self._record_operation_metrics( | ||
| operation=op, | ||
| result="failure", | ||
| duration_seconds=time.perf_counter() - started, | ||
| error_type="validation_error", | ||
| ) | ||
| self.logger.warning( | ||
| "Operation failed", | ||
| extra={"operation": op, "driver_type": self.driver_type, | ||
| "result": "failure", "error_type": "validation_error"}, | ||
| ) | ||
| await context.abort(StatusCode.INVALID_ARGUMENT, str(e)) | ||
| except TimeoutError as e: | ||
| self._record_operation_metrics( | ||
| operation=op, | ||
| result="failure", | ||
| duration_seconds=time.perf_counter() - started, | ||
| error_type="timeout", | ||
| ) | ||
| self.logger.warning( | ||
| "Operation failed", | ||
| extra={"operation": op, "driver_type": self.driver_type, | ||
| "result": "failure", "error_type": "timeout"}, | ||
| ) | ||
| await context.abort(StatusCode.DEADLINE_EXCEEDED, str(e)) | ||
| except ConnectionError as e: | ||
| self._record_operation_metrics( | ||
| operation=op, | ||
| result="failure", | ||
| duration_seconds=time.perf_counter() - started, | ||
| error_type="connection_error", | ||
| ) | ||
| self.logger.warning( | ||
| "Operation failed", | ||
| extra={"operation": op, "driver_type": self.driver_type, | ||
| "result": "failure", "error_type": "connection_error"}, | ||
| ) | ||
| await context.abort(StatusCode.UNAVAILABLE, str(e)) | ||
| except OSError as e: | ||
| self._record_operation_metrics( | ||
| operation=op, | ||
| result="failure", | ||
| duration_seconds=time.perf_counter() - started, | ||
| error_type="device_error", | ||
| ) | ||
| self.logger.warning( | ||
| "Operation failed", | ||
| extra={"operation": op, "driver_type": self.driver_type, | ||
| "result": "failure", "error_type": "device_error"}, | ||
| ) | ||
| await context.abort(StatusCode.INTERNAL, str(e)) | ||
| except Exception as e: | ||
| self._record_operation_metrics( | ||
| operation=op, | ||
| result="failure", | ||
| duration_seconds=time.perf_counter() - started, | ||
| error_type="internal_error", | ||
| ) | ||
| self.logger.warning( | ||
| "Operation failed", | ||
| extra={"operation": op, "driver_type": self.driver_type, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -57,11 +57,20 @@ class Session( | |
|
|
||
| @contextmanager | ||
| def __contextmanager__(self) -> Generator[Self]: | ||
| from jumpstarter.logging import set_log_context | ||
| from jumpstarter.metrics import get_registry | ||
|
|
||
| logging.getLogger().addHandler(self._logging_handler) | ||
| self.root_device.reset() | ||
| set_log_context(exporter=self.name) | ||
| get_registry().inc_active_sessions(exporter=self.name, delta=1.0) | ||
| try: | ||
| yield self | ||
| finally: | ||
| try: | ||
| get_registry().inc_active_sessions(exporter=self.name, delta=-1.0) | ||
| except Exception: | ||
| pass | ||
|
Comment on lines
+70
to
+73
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Log exception at WARNING or DEBUG level before suppressing it. |
||
| try: | ||
| self.root_device.close() | ||
| except Exception as e: | ||
|
|
@@ -319,14 +328,15 @@ async def StreamingDriverCall(self, request, context): | |
| async def Stream(self, _request_iterator, context): | ||
| request = StreamRequestMetadata(**dict(list(context.invocation_metadata()))).request | ||
| logger.debug("Streaming(%s)", request) | ||
| async with self[request.uuid].Stream(request, context) as stream: | ||
| driver = self[request.uuid] | ||
| async with driver.Stream(request, context) as stream: | ||
| metadata = [] | ||
| with suppress(TypedAttributeLookupError): | ||
| metadata.extend(stream.extra(MetadataStreamAttributes.metadata).items()) | ||
| await context.send_initial_metadata(metadata) | ||
|
|
||
| async with RouterStream(context=context) as remote: | ||
| async with forward_stream(remote, stream): | ||
| async with forward_stream(remote, stream, metrics_driver_type=driver.driver_type): | ||
| event = Event() | ||
| context.add_done_callback(lambda _: event.set()) | ||
| await event.wait() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| """Exporter-local Prometheus metrics (JEP-0013 Phase 2).""" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see we do use top level comments in some places, but i'd drop the JEP part at least, from the others as well |
||
|
|
||
| from .registry import ( | ||
| DEFAULT_EXEMPLAR_KEYS, | ||
| MetricsRegistry, | ||
| get_registry, | ||
| reset_registry_for_tests, | ||
| ) | ||
| from .server import start_metrics_server | ||
|
|
||
| __all__ = [ | ||
| "DEFAULT_EXEMPLAR_KEYS", | ||
| "MetricsRegistry", | ||
| "get_registry", | ||
| "reset_registry_for_tests", | ||
| "start_metrics_server", | ||
| ] | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider moving the imports to module level or caching the resolved functions. Capture the exporter name and exemplars once before the stream loop.