neuroglia-python 0.8.0__tar.gz → 0.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/PKG-INFO +1 -1
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/pyproject.toml +1 -1
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/__init__.py +7 -7
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/dependency_injection/service_provider.py +22 -10
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/observability/__init__.py +53 -0
- neuroglia_python-0.9.0/src/neuroglia/observability/decorators.py +185 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/observability/framework.py +84 -29
- neuroglia_python-0.9.0/src/neuroglia/observability/health_checks.py +350 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/observability/settings.py +7 -0
- neuroglia_python-0.9.0/src/neuroglia/observability/structlog_integration.py +242 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/LICENSE +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/README.md +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/a2a/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/a2a/protocol.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/application/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/application/background_scheduler.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/coordination/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/coordination/abstractions.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/coordination/leader_election.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/coordination/redis_backend.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/core/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/core/module_loader.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/core/operation_result.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/core/problem_details.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/core/type_extensions.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/core/type_finder.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/core/type_registry.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/abstractions.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/agent/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/agent/agent_events.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/agent/agent_state.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/conversation/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/conversation/execution_context.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/conversation/message.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/conversation/session.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/conversation/tool_call.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/exceptions.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/abstractions.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/event_sourcing/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/event_sourcing/abstractions.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/event_sourcing/event_sourcing_repository.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/event_sourcing/event_store/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/event_sourcing/event_store/event_store.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/event_sourcing/read_model_reconciliator.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/filesystem/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/filesystem/filesystem_repository.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/memory/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/memory/memory_repository.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/mongo/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/mongo/enhanced_mongo_repository.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/mongo/mongo_repository.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/mongo/motor_query.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/mongo/motor_repository.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/mongo/serialization_helper.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/mongo/typed_mongo_query.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/resources/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/resources/in_memory_storage_backend.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/resources/postgres_resource_store.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/resources/redis_resource_store.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/resources/resource_repository.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/tracing_mixin.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/queries/generic/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/queries/generic/get_by_id_query.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/queries/generic/list_query.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/queryable.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/resources/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/resources/abstractions.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/resources/controller.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/resources/serializers/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/resources/serializers/xml_serializer.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/resources/serializers/yaml_serializer.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/resources/state_machine.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/resources/watcher.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/unit_of_work.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/dependency_injection/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/dependency_injection/service_provider_backup.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/eventing/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/eventing/cloud_events/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/eventing/cloud_events/cloud_event.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/eventing/cloud_events/decorators.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/eventing/cloud_events/infrastructure/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_bus.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_ingestor.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_middleware.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_publisher.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/eventing/tracing.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/expressions/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/expressions/javascript_expression_translator.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/extensions/cqrs_metrics_extensions.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/extensions/mediator_extensions.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/extensions/state_persistence_extensions.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/hosting/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/hosting/abstractions.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/hosting/configuration/data_access_layer.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/hosting/web.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/integration/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/integration/cache_repository.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/integration/http_service_client.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/integration/models.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/logging/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/logging/logger.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/mapping/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/mapping/mapper.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/mediation/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/mediation/behaviors/domain_event_cloudevent_behavior.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/mediation/behaviors/domain_event_dispatching_middleware.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/mediation/mediator.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/mediation/metrics_middleware.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/mediation/pipeline_behavior.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/mediation/simple.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/mediation/tracing_middleware.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/mvc/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/mvc/controller_base.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/observability/logging.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/observability/metrics.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/observability/otel_sdk.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/observability/tracing.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/py.typed +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/reactive/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/reactive/rx_async.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/serialization/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/serialization/abstractions.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/serialization/aggregate_serializer.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/serialization/json.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/utils/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/utils/camel_model.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/utils/case_conversion.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/validation/__init__.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/validation/business_rules.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/validation/exceptions.py +0 -0
- {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/validation/validators.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "neuroglia-python"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.9.0"
|
|
4
4
|
description = "Port from .NET to Python of the Neuroglia Framework"
|
|
5
5
|
authors = ["Charles d'Avernas <charles.davernas@neuroglia.io>", "Bruno van de Werve <bvandewe@gmail.com>"]
|
|
6
6
|
license = "Apache"
|
|
@@ -32,18 +32,18 @@ __all__ = [
|
|
|
32
32
|
"Entity",
|
|
33
33
|
"DomainEvent",
|
|
34
34
|
"Repository",
|
|
35
|
-
# Agent module
|
|
35
|
+
# Agent module
|
|
36
36
|
"BaseAgentState",
|
|
37
37
|
"TeamMembership",
|
|
38
38
|
"KnowledgeScope",
|
|
39
39
|
"AgentCapability",
|
|
40
|
-
# A2A protocol
|
|
40
|
+
# A2A protocol
|
|
41
41
|
"TaskRequest",
|
|
42
42
|
"TaskResponse",
|
|
43
43
|
"AgentIdentity",
|
|
44
44
|
"TaskPriority",
|
|
45
45
|
"TaskStatus",
|
|
46
|
-
# Conversation building blocks
|
|
46
|
+
# Conversation building blocks
|
|
47
47
|
"Message",
|
|
48
48
|
"MessageRole",
|
|
49
49
|
"ToolCall",
|
|
@@ -80,7 +80,7 @@ __all__ = [
|
|
|
80
80
|
]
|
|
81
81
|
|
|
82
82
|
# Framework metadata
|
|
83
|
-
__version__ = "0.
|
|
83
|
+
__version__ = "0.9.0"
|
|
84
84
|
__author__ = "Neuroglia Team"
|
|
85
85
|
__email__ = "team@neuroglia.io"
|
|
86
86
|
__license__ = "Apache"
|
|
@@ -159,7 +159,7 @@ def __getattr__(name: str):
|
|
|
159
159
|
except ImportError:
|
|
160
160
|
pass
|
|
161
161
|
|
|
162
|
-
# Agent module
|
|
162
|
+
# Agent module
|
|
163
163
|
elif name in ["BaseAgentState", "TeamMembership", "KnowledgeScope", "AgentCapability"]:
|
|
164
164
|
try:
|
|
165
165
|
from .data.agent import (
|
|
@@ -180,7 +180,7 @@ def __getattr__(name: str):
|
|
|
180
180
|
except ImportError:
|
|
181
181
|
pass
|
|
182
182
|
|
|
183
|
-
# A2A protocol
|
|
183
|
+
# A2A protocol
|
|
184
184
|
elif name in ["TaskRequest", "TaskResponse", "AgentIdentity", "TaskPriority", "TaskStatus"]:
|
|
185
185
|
try:
|
|
186
186
|
from .a2a import (
|
|
@@ -204,7 +204,7 @@ def __getattr__(name: str):
|
|
|
204
204
|
except ImportError:
|
|
205
205
|
pass
|
|
206
206
|
|
|
207
|
-
# Conversation building blocks
|
|
207
|
+
# Conversation building blocks
|
|
208
208
|
elif name in ["Message", "MessageRole", "ToolCall", "ToolResult", "ExecutionContext", "Session"]:
|
|
209
209
|
try:
|
|
210
210
|
from .data.conversation import (
|
|
@@ -375,7 +375,7 @@ class ServiceScope(ServiceScopeBase, ServiceProviderBase):
|
|
|
375
375
|
try:
|
|
376
376
|
service = self._build_service(descriptor)
|
|
377
377
|
transient_services.append(service)
|
|
378
|
-
except Exception
|
|
378
|
+
except Exception:
|
|
379
379
|
# If building fails, skip this service
|
|
380
380
|
pass
|
|
381
381
|
|
|
@@ -572,6 +572,10 @@ class ServiceProvider(ServiceProviderBase):
|
|
|
572
572
|
return result_services
|
|
573
573
|
|
|
574
574
|
def _is_service_instance_of(self, service: Any, type_: type) -> bool:
|
|
575
|
+
# Handle None or invalid types (e.g., inspect._empty from untyped factories)
|
|
576
|
+
if type_ is None or not isinstance(type_, type) and not hasattr(type_, "__origin__"):
|
|
577
|
+
# Cannot determine type - fall back to checking if service exists
|
|
578
|
+
return service is not None
|
|
575
579
|
if hasattr(type_, "__origin__"):
|
|
576
580
|
service_type = service.__orig_class__ if hasattr(service, "__orig_class__") else type(service)
|
|
577
581
|
service_generic_arguments = TypeExtensions.get_generic_arguments(service_type)
|
|
@@ -750,17 +754,25 @@ class ServiceDescriptor:
|
|
|
750
754
|
lifetime: ServiceLifetime = ServiceLifetime.SINGLETON
|
|
751
755
|
""" Gets the service's lifetime. Defaults to 'SINGLETON' """
|
|
752
756
|
|
|
753
|
-
def get_implementation_type(self) -> type:
|
|
754
|
-
"""Gets the service's implementation type
|
|
757
|
+
def get_implementation_type(self) -> type | None:
|
|
758
|
+
"""Gets the service's implementation type.
|
|
759
|
+
|
|
760
|
+
Returns:
|
|
761
|
+
The implementation type, or None if it cannot be determined
|
|
762
|
+
(e.g., untyped lambda factories).
|
|
763
|
+
"""
|
|
755
764
|
if self.implementation_type is not None:
|
|
756
765
|
return self.implementation_type
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
766
|
+
if self.singleton is not None:
|
|
767
|
+
return type(self.singleton)
|
|
768
|
+
if self.implementation_factory is not None:
|
|
769
|
+
return_annotation = inspect.signature(self.implementation_factory).return_annotation
|
|
770
|
+
# Handle missing annotations (inspect._empty) or string annotations
|
|
771
|
+
if return_annotation is not inspect.Parameter.empty and isinstance(return_annotation, type):
|
|
772
|
+
return return_annotation
|
|
773
|
+
# For string annotations or forward references, return None
|
|
774
|
+
# The caller should handle this gracefully
|
|
775
|
+
return None
|
|
764
776
|
|
|
765
777
|
|
|
766
778
|
# ServiceCollection will be defined at the end of this file
|
|
@@ -44,7 +44,24 @@ Usage:
|
|
|
44
44
|
pass
|
|
45
45
|
"""
|
|
46
46
|
|
|
47
|
+
# Metrics Decorators (CR-3)
|
|
48
|
+
from neuroglia.observability.decorators import (
|
|
49
|
+
track_latency,
|
|
50
|
+
track_operation,
|
|
51
|
+
track_operation_and_latency,
|
|
52
|
+
)
|
|
47
53
|
from neuroglia.observability.framework import Observability
|
|
54
|
+
|
|
55
|
+
# Health Check Providers (CR-2)
|
|
56
|
+
from neuroglia.observability.health_checks import (
|
|
57
|
+
HealthCheckProvider,
|
|
58
|
+
HealthCheckResult,
|
|
59
|
+
HttpServiceHealthCheck,
|
|
60
|
+
MongoDBHealthCheck,
|
|
61
|
+
Neo4jHealthCheck,
|
|
62
|
+
QdrantHealthCheck,
|
|
63
|
+
RedisHealthCheck,
|
|
64
|
+
)
|
|
48
65
|
from neuroglia.observability.logging import (
|
|
49
66
|
configure_logging,
|
|
50
67
|
get_logger_with_trace_context,
|
|
@@ -80,6 +97,19 @@ from neuroglia.observability.tracing import (
|
|
|
80
97
|
trace_sync,
|
|
81
98
|
)
|
|
82
99
|
|
|
100
|
+
# Structlog Integration (CR-1) - Optional
|
|
101
|
+
try:
|
|
102
|
+
from neuroglia.observability.structlog_integration import (
|
|
103
|
+
bind_contextvars,
|
|
104
|
+
clear_contextvars,
|
|
105
|
+
configure_structlog,
|
|
106
|
+
get_structlog_logger,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
_STRUCTLOG_AVAILABLE = True
|
|
110
|
+
except ImportError:
|
|
111
|
+
_STRUCTLOG_AVAILABLE = False
|
|
112
|
+
|
|
83
113
|
__all__ = [
|
|
84
114
|
# New Framework-Style Configuration
|
|
85
115
|
"Observability",
|
|
@@ -112,4 +142,27 @@ __all__ = [
|
|
|
112
142
|
# Prometheus and HTTP Instrumentation
|
|
113
143
|
"instrument_fastapi_app",
|
|
114
144
|
"add_metrics_endpoint",
|
|
145
|
+
# Health Check Providers (CR-2)
|
|
146
|
+
"HealthCheckProvider",
|
|
147
|
+
"HealthCheckResult",
|
|
148
|
+
"MongoDBHealthCheck",
|
|
149
|
+
"RedisHealthCheck",
|
|
150
|
+
"Neo4jHealthCheck",
|
|
151
|
+
"QdrantHealthCheck",
|
|
152
|
+
"HttpServiceHealthCheck",
|
|
153
|
+
# Metrics Decorators (CR-3)
|
|
154
|
+
"track_operation",
|
|
155
|
+
"track_latency",
|
|
156
|
+
"track_operation_and_latency",
|
|
115
157
|
]
|
|
158
|
+
|
|
159
|
+
# Add structlog exports if available (CR-1)
|
|
160
|
+
if _STRUCTLOG_AVAILABLE:
|
|
161
|
+
__all__.extend(
|
|
162
|
+
[
|
|
163
|
+
"configure_structlog",
|
|
164
|
+
"get_structlog_logger",
|
|
165
|
+
"bind_contextvars",
|
|
166
|
+
"clear_contextvars",
|
|
167
|
+
]
|
|
168
|
+
)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Decorators for automatic metrics recording.
|
|
3
|
+
|
|
4
|
+
Provides convenience decorators for tracking operation counts and latencies
|
|
5
|
+
without manual instrumentation in every handler.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from neuroglia.observability.decorators import track_operation, track_latency
|
|
9
|
+
|
|
10
|
+
@track_operation("skills_manager.skills.created")
|
|
11
|
+
async def handle_async(self, command):
|
|
12
|
+
# Your logic here
|
|
13
|
+
...
|
|
14
|
+
|
|
15
|
+
@track_latency("api.request")
|
|
16
|
+
async def endpoint(self, request):
|
|
17
|
+
# Your logic here
|
|
18
|
+
...
|
|
19
|
+
"""
|
|
20
|
+
import asyncio
|
|
21
|
+
import time
|
|
22
|
+
from collections.abc import Callable
|
|
23
|
+
from functools import wraps
|
|
24
|
+
from typing import ParamSpec, TypeVar
|
|
25
|
+
|
|
26
|
+
from neuroglia.observability.metrics import create_counter, create_histogram
|
|
27
|
+
|
|
28
|
+
P = ParamSpec("P")
|
|
29
|
+
T = TypeVar("T")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def track_operation(
|
|
33
|
+
metric_name: str,
|
|
34
|
+
success_status: str = "success",
|
|
35
|
+
error_status: str = "error",
|
|
36
|
+
) -> Callable[[Callable[P, T]], Callable[P, T]]:
|
|
37
|
+
"""Decorator to track operation count and status.
|
|
38
|
+
|
|
39
|
+
Creates a counter metric that increments on each call, with a status
|
|
40
|
+
label indicating success or error.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
metric_name: Base name for the metric (e.g., "skills_manager.skills.created").
|
|
44
|
+
success_status: Status label for successful operations (default: "success").
|
|
45
|
+
error_status: Status label for failed operations (default: "error").
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Decorated function with automatic operation tracking.
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
>>> @track_operation("skills_manager.skills.created")
|
|
52
|
+
... async def create_skill(self, command):
|
|
53
|
+
... # On success: counter increments with status="success"
|
|
54
|
+
... # On exception: counter increments with status="error"
|
|
55
|
+
... return result
|
|
56
|
+
"""
|
|
57
|
+
# Create counter lazily on first call
|
|
58
|
+
counter = create_counter(
|
|
59
|
+
name=metric_name,
|
|
60
|
+
description=f"Total {metric_name.split('.')[-1]} operations",
|
|
61
|
+
unit="1",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def decorator(func: Callable[P, T]) -> Callable[P, T]:
|
|
65
|
+
if asyncio.iscoroutinefunction(func):
|
|
66
|
+
|
|
67
|
+
@wraps(func)
|
|
68
|
+
async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
69
|
+
try:
|
|
70
|
+
result = await func(*args, **kwargs)
|
|
71
|
+
counter.add(1, {"status": success_status})
|
|
72
|
+
return result
|
|
73
|
+
except Exception:
|
|
74
|
+
counter.add(1, {"status": error_status})
|
|
75
|
+
raise
|
|
76
|
+
|
|
77
|
+
return async_wrapper # type: ignore[return-value]
|
|
78
|
+
else:
|
|
79
|
+
|
|
80
|
+
@wraps(func)
|
|
81
|
+
def sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
82
|
+
try:
|
|
83
|
+
result = func(*args, **kwargs)
|
|
84
|
+
counter.add(1, {"status": success_status})
|
|
85
|
+
return result
|
|
86
|
+
except Exception:
|
|
87
|
+
counter.add(1, {"status": error_status})
|
|
88
|
+
raise
|
|
89
|
+
|
|
90
|
+
return sync_wrapper # type: ignore[return-value]
|
|
91
|
+
|
|
92
|
+
return decorator
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def track_latency(
|
|
96
|
+
metric_name: str,
|
|
97
|
+
unit: str = "ms",
|
|
98
|
+
) -> Callable[[Callable[P, T]], Callable[P, T]]:
|
|
99
|
+
"""Decorator to track operation latency.
|
|
100
|
+
|
|
101
|
+
Creates a histogram metric that records the duration of each call.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
metric_name: Base name for the metric (e.g., "api.request").
|
|
105
|
+
unit: Unit of measurement (default: "ms").
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Decorated function with automatic latency tracking.
|
|
109
|
+
|
|
110
|
+
Example:
|
|
111
|
+
>>> @track_latency("api.request")
|
|
112
|
+
... async def handle_request(self, request):
|
|
113
|
+
... # Duration is recorded as histogram after completion
|
|
114
|
+
... return response
|
|
115
|
+
"""
|
|
116
|
+
# Create histogram lazily on first call
|
|
117
|
+
histogram = create_histogram(
|
|
118
|
+
name=f"{metric_name}.latency",
|
|
119
|
+
description=f"{metric_name} latency",
|
|
120
|
+
unit=unit,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def decorator(func: Callable[P, T]) -> Callable[P, T]:
|
|
124
|
+
if asyncio.iscoroutinefunction(func):
|
|
125
|
+
|
|
126
|
+
@wraps(func)
|
|
127
|
+
async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
128
|
+
start = time.perf_counter()
|
|
129
|
+
try:
|
|
130
|
+
return await func(*args, **kwargs)
|
|
131
|
+
finally:
|
|
132
|
+
elapsed_ms = (time.perf_counter() - start) * 1000
|
|
133
|
+
histogram.record(elapsed_ms)
|
|
134
|
+
|
|
135
|
+
return async_wrapper # type: ignore[return-value]
|
|
136
|
+
else:
|
|
137
|
+
|
|
138
|
+
@wraps(func)
|
|
139
|
+
def sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
140
|
+
start = time.perf_counter()
|
|
141
|
+
try:
|
|
142
|
+
return func(*args, **kwargs)
|
|
143
|
+
finally:
|
|
144
|
+
elapsed_ms = (time.perf_counter() - start) * 1000
|
|
145
|
+
histogram.record(elapsed_ms)
|
|
146
|
+
|
|
147
|
+
return sync_wrapper # type: ignore[return-value]
|
|
148
|
+
|
|
149
|
+
return decorator
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def track_operation_and_latency(
|
|
153
|
+
metric_name: str,
|
|
154
|
+
success_status: str = "success",
|
|
155
|
+
error_status: str = "error",
|
|
156
|
+
latency_unit: str = "ms",
|
|
157
|
+
) -> Callable[[Callable[P, T]], Callable[P, T]]:
|
|
158
|
+
"""Combined decorator to track both operation count and latency.
|
|
159
|
+
|
|
160
|
+
Combines the functionality of @track_operation and @track_latency
|
|
161
|
+
into a single decorator for convenience.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
metric_name: Base name for both metrics.
|
|
165
|
+
success_status: Status label for successful operations.
|
|
166
|
+
error_status: Status label for failed operations.
|
|
167
|
+
latency_unit: Unit for latency measurement (default: "ms").
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Decorated function with both operation and latency tracking.
|
|
171
|
+
|
|
172
|
+
Example:
|
|
173
|
+
>>> @track_operation_and_latency("skills_manager.skills.created")
|
|
174
|
+
... async def create_skill(self, command):
|
|
175
|
+
... # Records both count and latency metrics
|
|
176
|
+
... return result
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
def decorator(func: Callable[P, T]) -> Callable[P, T]:
|
|
180
|
+
# Apply both decorators
|
|
181
|
+
decorated = track_latency(metric_name, latency_unit)(func)
|
|
182
|
+
decorated = track_operation(metric_name, success_status, error_status)(decorated)
|
|
183
|
+
return decorated # type: ignore[return-value]
|
|
184
|
+
|
|
185
|
+
return decorator
|
|
@@ -8,12 +8,13 @@ comprehensive observability with minimal configuration.
|
|
|
8
8
|
|
|
9
9
|
import datetime
|
|
10
10
|
import logging
|
|
11
|
-
from typing import TYPE_CHECKING
|
|
11
|
+
from typing import TYPE_CHECKING, Optional
|
|
12
12
|
|
|
13
13
|
from fastapi import FastAPI, Response
|
|
14
14
|
|
|
15
15
|
if TYPE_CHECKING:
|
|
16
16
|
from neuroglia.hosting.web import WebApplicationBuilder
|
|
17
|
+
from neuroglia.observability.health_checks import HealthCheckProvider
|
|
17
18
|
|
|
18
19
|
from neuroglia.observability.settings import (
|
|
19
20
|
ObservabilityConfig,
|
|
@@ -44,13 +45,20 @@ class Observability:
|
|
|
44
45
|
"""
|
|
45
46
|
|
|
46
47
|
@classmethod
|
|
47
|
-
def configure(
|
|
48
|
+
def configure(
|
|
49
|
+
cls,
|
|
50
|
+
builder: "WebApplicationBuilder",
|
|
51
|
+
auto_enable_cqrs_metrics: bool = True,
|
|
52
|
+
health_check_providers: Optional[list["HealthCheckProvider"]] = None,
|
|
53
|
+
**overrides,
|
|
54
|
+
) -> None:
|
|
48
55
|
"""
|
|
49
56
|
Configure comprehensive observability for the application.
|
|
50
57
|
|
|
51
58
|
Args:
|
|
52
59
|
builder: The enhanced web application builder (must contain app_settings)
|
|
53
60
|
auto_enable_cqrs_metrics: Auto-enable CQRS metrics if Mediator is detected (default: True)
|
|
61
|
+
health_check_providers: List of HealthCheckProvider instances for dependency monitoring
|
|
54
62
|
**overrides: Optional configuration overrides (tracing_enabled=False, etc.)
|
|
55
63
|
"""
|
|
56
64
|
app_settings = builder.app_settings
|
|
@@ -62,6 +70,9 @@ class Observability:
|
|
|
62
70
|
# Create observability configuration from app_settings
|
|
63
71
|
config = ObservabilityConfig(app_settings, **overrides)
|
|
64
72
|
|
|
73
|
+
# Store health check providers on config for use by endpoints
|
|
74
|
+
config.health_check_providers = health_check_providers or []
|
|
75
|
+
|
|
65
76
|
# Register configuration in DI container for access throughout application
|
|
66
77
|
builder.services.add_singleton(ObservabilityConfig, lambda: config)
|
|
67
78
|
|
|
@@ -155,11 +166,46 @@ class Observability:
|
|
|
155
166
|
|
|
156
167
|
log.info(f"🔭 OpenTelemetry configured: endpoint={config.otel_endpoint}")
|
|
157
168
|
|
|
169
|
+
# Create service info gauge (CR-4)
|
|
170
|
+
if getattr(config, "service_info_gauge", True):
|
|
171
|
+
cls._create_service_info_gauge(config)
|
|
172
|
+
|
|
158
173
|
except ImportError as e:
|
|
159
174
|
log.error(f"❌ OpenTelemetry configuration failed - missing dependencies: {e}")
|
|
160
175
|
except Exception as e:
|
|
161
176
|
log.error(f"❌ OpenTelemetry configuration error: {e}")
|
|
162
177
|
|
|
178
|
+
@classmethod
|
|
179
|
+
def _create_service_info_gauge(cls, config: ObservabilityConfig) -> None:
|
|
180
|
+
"""Create service info gauge metric for service discovery (CR-4)"""
|
|
181
|
+
try:
|
|
182
|
+
from opentelemetry.metrics import Observation
|
|
183
|
+
|
|
184
|
+
from neuroglia.observability.metrics import create_observable_gauge
|
|
185
|
+
|
|
186
|
+
def _service_info_callback(options):
|
|
187
|
+
return [
|
|
188
|
+
Observation(
|
|
189
|
+
1,
|
|
190
|
+
{
|
|
191
|
+
"service.name": config.service_name,
|
|
192
|
+
"service.version": config.service_version,
|
|
193
|
+
"deployment.environment": config.deployment_environment,
|
|
194
|
+
},
|
|
195
|
+
)
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
create_observable_gauge(
|
|
199
|
+
name="service.info",
|
|
200
|
+
callback=_service_info_callback,
|
|
201
|
+
unit="1",
|
|
202
|
+
description="Service metadata gauge for discovery",
|
|
203
|
+
)
|
|
204
|
+
log.info("🏷️ Service info gauge created")
|
|
205
|
+
|
|
206
|
+
except Exception as e:
|
|
207
|
+
log.warning(f"⚠️ Could not create service info gauge: {e}")
|
|
208
|
+
|
|
163
209
|
@classmethod
|
|
164
210
|
def _register_standard_endpoints(cls, builder: "WebApplicationBuilder", config: ObservabilityConfig) -> None:
|
|
165
211
|
"""Register standard endpoints for automatic addition to main app during build"""
|
|
@@ -187,19 +233,36 @@ class StandardEndpoints:
|
|
|
187
233
|
health_status = {"status": "healthy", "timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(), "service": {"name": config.service_name, "version": config.service_version, "environment": config.deployment_environment}}
|
|
188
234
|
|
|
189
235
|
# Add dependency checks if configured
|
|
190
|
-
|
|
236
|
+
providers = getattr(config, "health_check_providers", [])
|
|
237
|
+
if config.health_checks or providers:
|
|
191
238
|
dependencies = {}
|
|
192
239
|
overall_healthy = True
|
|
193
240
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
241
|
+
# Check all registered health check providers
|
|
242
|
+
for provider in providers:
|
|
243
|
+
try:
|
|
244
|
+
result = await provider.check()
|
|
245
|
+
dep_status = {
|
|
246
|
+
"status": result.status,
|
|
247
|
+
}
|
|
248
|
+
if result.message:
|
|
249
|
+
dep_status["message"] = result.message
|
|
250
|
+
if result.latency_ms is not None:
|
|
251
|
+
dep_status["latency_ms"] = round(result.latency_ms, 2)
|
|
252
|
+
dependencies[provider.name] = dep_status
|
|
253
|
+
|
|
254
|
+
if result.status != "healthy":
|
|
255
|
+
overall_healthy = False
|
|
256
|
+
except Exception as e:
|
|
257
|
+
log.warning(f"Health check failed for {provider.name}: {e}")
|
|
258
|
+
dependencies[provider.name] = {"status": "unhealthy", "message": str(e)}
|
|
201
259
|
overall_healthy = False
|
|
202
260
|
|
|
261
|
+
# Handle string-based health checks without providers (legacy)
|
|
262
|
+
for dependency in config.health_checks:
|
|
263
|
+
if dependency not in dependencies:
|
|
264
|
+
dependencies[dependency] = {"status": "healthy", "message": "No provider configured"}
|
|
265
|
+
|
|
203
266
|
health_status["dependencies"] = dependencies
|
|
204
267
|
if not overall_healthy:
|
|
205
268
|
health_status["status"] = "degraded"
|
|
@@ -222,10 +285,15 @@ class StandardEndpoints:
|
|
|
222
285
|
# Add basic readiness checks
|
|
223
286
|
checks = {"application": "ready"}
|
|
224
287
|
|
|
225
|
-
#
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
288
|
+
# Use health check providers for readiness
|
|
289
|
+
providers = getattr(config, "health_check_providers", [])
|
|
290
|
+
for provider in providers:
|
|
291
|
+
try:
|
|
292
|
+
result = await provider.check()
|
|
293
|
+
checks[provider.name] = "ready" if result.status == "healthy" else "not_ready"
|
|
294
|
+
except Exception as e:
|
|
295
|
+
log.warning(f"Readiness check failed for {provider.name}: {e}")
|
|
296
|
+
checks[provider.name] = "not_ready"
|
|
229
297
|
|
|
230
298
|
ready_status["checks"] = checks
|
|
231
299
|
return ready_status
|
|
@@ -249,18 +317,5 @@ class StandardEndpoints:
|
|
|
249
317
|
"""Fallback metrics endpoint when Prometheus is not available"""
|
|
250
318
|
return Response(content="# Prometheus metrics not available\n# Install prometheus-client for full metrics support\n", media_type="text/plain")
|
|
251
319
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
"""
|
|
255
|
-
Check health of a specific dependency.
|
|
256
|
-
|
|
257
|
-
TODO: Implement pluggable health check providers for:
|
|
258
|
-
- MongoDB (check connection)
|
|
259
|
-
- Redis (ping)
|
|
260
|
-
- Keycloak (check auth endpoint)
|
|
261
|
-
- HTTP services (health endpoint check)
|
|
262
|
-
- Database connections
|
|
263
|
-
"""
|
|
264
|
-
# Placeholder implementation - always return healthy
|
|
265
|
-
# In real implementation, this would check actual dependency status
|
|
266
|
-
return "healthy"
|
|
320
|
+
# Note: _check_dependency_health has been replaced by HealthCheckProvider implementations
|
|
321
|
+
# in neuroglia/observability/health_checks.py (CR-2)
|