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.
Files changed (133) hide show
  1. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/PKG-INFO +1 -1
  2. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/pyproject.toml +1 -1
  3. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/__init__.py +7 -7
  4. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/dependency_injection/service_provider.py +22 -10
  5. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/observability/__init__.py +53 -0
  6. neuroglia_python-0.9.0/src/neuroglia/observability/decorators.py +185 -0
  7. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/observability/framework.py +84 -29
  8. neuroglia_python-0.9.0/src/neuroglia/observability/health_checks.py +350 -0
  9. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/observability/settings.py +7 -0
  10. neuroglia_python-0.9.0/src/neuroglia/observability/structlog_integration.py +242 -0
  11. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/LICENSE +0 -0
  12. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/README.md +0 -0
  13. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/a2a/__init__.py +0 -0
  14. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/a2a/protocol.py +0 -0
  15. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/application/__init__.py +0 -0
  16. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/application/background_scheduler.py +0 -0
  17. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/coordination/__init__.py +0 -0
  18. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/coordination/abstractions.py +0 -0
  19. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/coordination/leader_election.py +0 -0
  20. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/coordination/redis_backend.py +0 -0
  21. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/core/__init__.py +0 -0
  22. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/core/module_loader.py +0 -0
  23. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/core/operation_result.py +0 -0
  24. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/core/problem_details.py +0 -0
  25. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/core/type_extensions.py +0 -0
  26. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/core/type_finder.py +0 -0
  27. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/core/type_registry.py +0 -0
  28. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/__init__.py +0 -0
  29. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/abstractions.py +0 -0
  30. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/agent/__init__.py +0 -0
  31. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/agent/agent_events.py +0 -0
  32. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/agent/agent_state.py +0 -0
  33. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/conversation/__init__.py +0 -0
  34. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/conversation/execution_context.py +0 -0
  35. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/conversation/message.py +0 -0
  36. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/conversation/session.py +0 -0
  37. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/conversation/tool_call.py +0 -0
  38. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/exceptions.py +0 -0
  39. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/__init__.py +0 -0
  40. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/abstractions.py +0 -0
  41. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/event_sourcing/__init__.py +0 -0
  42. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/event_sourcing/abstractions.py +0 -0
  43. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/event_sourcing/event_sourcing_repository.py +0 -0
  44. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/event_sourcing/event_store/__init__.py +0 -0
  45. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/event_sourcing/event_store/event_store.py +0 -0
  46. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/event_sourcing/read_model_reconciliator.py +0 -0
  47. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/filesystem/__init__.py +0 -0
  48. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/filesystem/filesystem_repository.py +0 -0
  49. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/memory/__init__.py +0 -0
  50. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/memory/memory_repository.py +0 -0
  51. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/mongo/__init__.py +0 -0
  52. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/mongo/enhanced_mongo_repository.py +0 -0
  53. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/mongo/mongo_repository.py +0 -0
  54. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/mongo/motor_query.py +0 -0
  55. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/mongo/motor_repository.py +0 -0
  56. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/mongo/serialization_helper.py +0 -0
  57. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/mongo/typed_mongo_query.py +0 -0
  58. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/resources/__init__.py +0 -0
  59. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/resources/in_memory_storage_backend.py +0 -0
  60. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/resources/postgres_resource_store.py +0 -0
  61. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/resources/redis_resource_store.py +0 -0
  62. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/resources/resource_repository.py +0 -0
  63. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/infrastructure/tracing_mixin.py +0 -0
  64. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/queries/generic/__init__.py +0 -0
  65. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/queries/generic/get_by_id_query.py +0 -0
  66. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/queries/generic/list_query.py +0 -0
  67. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/queryable.py +0 -0
  68. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/resources/__init__.py +0 -0
  69. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/resources/abstractions.py +0 -0
  70. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/resources/controller.py +0 -0
  71. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/resources/serializers/__init__.py +0 -0
  72. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/resources/serializers/xml_serializer.py +0 -0
  73. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/resources/serializers/yaml_serializer.py +0 -0
  74. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/resources/state_machine.py +0 -0
  75. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/resources/watcher.py +0 -0
  76. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/data/unit_of_work.py +0 -0
  77. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/dependency_injection/__init__.py +0 -0
  78. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/dependency_injection/service_provider_backup.py +0 -0
  79. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/eventing/__init__.py +0 -0
  80. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/eventing/cloud_events/__init__.py +0 -0
  81. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/eventing/cloud_events/cloud_event.py +0 -0
  82. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/eventing/cloud_events/decorators.py +0 -0
  83. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/eventing/cloud_events/infrastructure/__init__.py +0 -0
  84. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_bus.py +0 -0
  85. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_ingestor.py +0 -0
  86. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_middleware.py +0 -0
  87. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/eventing/cloud_events/infrastructure/cloud_event_publisher.py +0 -0
  88. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/eventing/tracing.py +0 -0
  89. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/expressions/__init__.py +0 -0
  90. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/expressions/javascript_expression_translator.py +0 -0
  91. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/extensions/cqrs_metrics_extensions.py +0 -0
  92. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/extensions/mediator_extensions.py +0 -0
  93. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/extensions/state_persistence_extensions.py +0 -0
  94. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/hosting/__init__.py +0 -0
  95. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/hosting/abstractions.py +0 -0
  96. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/hosting/configuration/data_access_layer.py +0 -0
  97. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/hosting/web.py +0 -0
  98. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/integration/__init__.py +0 -0
  99. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/integration/cache_repository.py +0 -0
  100. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/integration/http_service_client.py +0 -0
  101. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/integration/models.py +0 -0
  102. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/logging/__init__.py +0 -0
  103. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/logging/logger.py +0 -0
  104. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/mapping/__init__.py +0 -0
  105. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/mapping/mapper.py +0 -0
  106. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/mediation/__init__.py +0 -0
  107. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/mediation/behaviors/domain_event_cloudevent_behavior.py +0 -0
  108. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/mediation/behaviors/domain_event_dispatching_middleware.py +0 -0
  109. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/mediation/mediator.py +0 -0
  110. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/mediation/metrics_middleware.py +0 -0
  111. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/mediation/pipeline_behavior.py +0 -0
  112. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/mediation/simple.py +0 -0
  113. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/mediation/tracing_middleware.py +0 -0
  114. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/mvc/__init__.py +0 -0
  115. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/mvc/controller_base.py +0 -0
  116. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/observability/logging.py +0 -0
  117. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/observability/metrics.py +0 -0
  118. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/observability/otel_sdk.py +0 -0
  119. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/observability/tracing.py +0 -0
  120. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/py.typed +0 -0
  121. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/reactive/__init__.py +0 -0
  122. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/reactive/rx_async.py +0 -0
  123. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/serialization/__init__.py +0 -0
  124. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/serialization/abstractions.py +0 -0
  125. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/serialization/aggregate_serializer.py +0 -0
  126. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/serialization/json.py +0 -0
  127. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/utils/__init__.py +0 -0
  128. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/utils/camel_model.py +0 -0
  129. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/utils/case_conversion.py +0 -0
  130. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/validation/__init__.py +0 -0
  131. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/validation/business_rules.py +0 -0
  132. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/validation/exceptions.py +0 -0
  133. {neuroglia_python-0.8.0 → neuroglia_python-0.9.0}/src/neuroglia/validation/validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: neuroglia-python
3
- Version: 0.8.0
3
+ Version: 0.9.0
4
4
  Summary: Port from .NET to Python of the Neuroglia Framework
5
5
  License: Apache
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "neuroglia-python"
3
- version = "0.8.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 (new in 0.8.0)
35
+ # Agent module
36
36
  "BaseAgentState",
37
37
  "TeamMembership",
38
38
  "KnowledgeScope",
39
39
  "AgentCapability",
40
- # A2A protocol (new in 0.8.0)
40
+ # A2A protocol
41
41
  "TaskRequest",
42
42
  "TaskResponse",
43
43
  "AgentIdentity",
44
44
  "TaskPriority",
45
45
  "TaskStatus",
46
- # Conversation building blocks (new in 0.8.0)
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.8.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 (new in 0.8.0)
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 (new in 0.8.0)
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 (new in 0.8.0)
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 as ex:
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
- return_type = inspect.signature(self.implementation_factory).return_annotation if self.implementation_factory != None else None
758
- if return_type is None and self.implementation_factory != None:
759
- if self.implementation_type is None:
760
- raise Exception(f"Failed to determine the return type of the implementation factory configured for service of type '{self.service_type.__name__}'. Either specify the implementation type, or use a function instead of a lambda as factory callable.")
761
- else:
762
- return_type = self.implementation_type
763
- return type(self.singleton) if self.singleton is not None else inspect.signature(self.implementation_factory).return_annotation
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(cls, builder: "WebApplicationBuilder", auto_enable_cqrs_metrics: bool = True, **overrides) -> None:
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
- if config.health_checks:
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
- for dependency in config.health_checks:
195
- # TODO: Implement pluggable health check providers
196
- # For now, assume dependencies are healthy
197
- dependency_status = await StandardEndpoints._check_dependency_health(dependency)
198
- dependencies[dependency] = dependency_status
199
-
200
- if dependency_status != "healthy":
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
- # TODO: Add pluggable readiness checks
226
- if config.health_checks:
227
- for dependency in config.health_checks:
228
- checks[dependency] = "ready" # Placeholder
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
- @staticmethod
253
- async def _check_dependency_health(dependency_name: str) -> str:
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)