omnibase_infra 0.3.1__py3-none-any.whl → 0.4.0__py3-none-any.whl

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 (117) hide show
  1. omnibase_infra/__init__.py +1 -1
  2. omnibase_infra/enums/__init__.py +3 -0
  3. omnibase_infra/enums/enum_consumer_group_purpose.py +9 -0
  4. omnibase_infra/enums/enum_postgres_error_code.py +188 -0
  5. omnibase_infra/errors/__init__.py +4 -0
  6. omnibase_infra/errors/error_infra.py +60 -0
  7. omnibase_infra/handlers/__init__.py +3 -0
  8. omnibase_infra/handlers/handler_slack_webhook.py +426 -0
  9. omnibase_infra/handlers/models/__init__.py +14 -0
  10. omnibase_infra/handlers/models/enum_alert_severity.py +36 -0
  11. omnibase_infra/handlers/models/model_slack_alert.py +24 -0
  12. omnibase_infra/handlers/models/model_slack_alert_payload.py +77 -0
  13. omnibase_infra/handlers/models/model_slack_alert_result.py +73 -0
  14. omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +29 -20
  15. omnibase_infra/mixins/__init__.py +14 -0
  16. omnibase_infra/mixins/mixin_node_introspection.py +42 -20
  17. omnibase_infra/mixins/mixin_postgres_error_response.py +314 -0
  18. omnibase_infra/mixins/mixin_postgres_op_executor.py +298 -0
  19. omnibase_infra/models/__init__.py +3 -0
  20. omnibase_infra/models/discovery/model_dependency_spec.py +1 -0
  21. omnibase_infra/models/discovery/model_discovered_capabilities.py +1 -1
  22. omnibase_infra/models/discovery/model_introspection_config.py +28 -1
  23. omnibase_infra/models/discovery/model_introspection_performance_metrics.py +1 -0
  24. omnibase_infra/models/discovery/model_introspection_task_config.py +1 -0
  25. omnibase_infra/{nodes/effects/models → models}/model_backend_result.py +22 -6
  26. omnibase_infra/models/projection/__init__.py +11 -0
  27. omnibase_infra/models/projection/model_contract_projection.py +170 -0
  28. omnibase_infra/models/projection/model_topic_projection.py +148 -0
  29. omnibase_infra/models/runtime/__init__.py +4 -0
  30. omnibase_infra/models/runtime/model_resolved_dependencies.py +116 -0
  31. omnibase_infra/nodes/contract_registry_reducer/__init__.py +5 -0
  32. omnibase_infra/nodes/contract_registry_reducer/contract.yaml +6 -5
  33. omnibase_infra/nodes/contract_registry_reducer/contract_registration_event_router.py +689 -0
  34. omnibase_infra/nodes/contract_registry_reducer/reducer.py +9 -26
  35. omnibase_infra/nodes/effects/__init__.py +1 -1
  36. omnibase_infra/nodes/effects/models/__init__.py +6 -4
  37. omnibase_infra/nodes/effects/models/model_registry_response.py +1 -1
  38. omnibase_infra/nodes/effects/protocol_consul_client.py +1 -1
  39. omnibase_infra/nodes/effects/protocol_postgres_adapter.py +1 -1
  40. omnibase_infra/nodes/effects/registry_effect.py +1 -1
  41. omnibase_infra/nodes/node_contract_persistence_effect/__init__.py +101 -0
  42. omnibase_infra/nodes/node_contract_persistence_effect/contract.yaml +490 -0
  43. omnibase_infra/nodes/node_contract_persistence_effect/handlers/__init__.py +74 -0
  44. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_cleanup_topics.py +217 -0
  45. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_contract_upsert.py +242 -0
  46. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_deactivate.py +194 -0
  47. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_heartbeat.py +243 -0
  48. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_mark_stale.py +208 -0
  49. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_topic_update.py +298 -0
  50. omnibase_infra/nodes/node_contract_persistence_effect/models/__init__.py +15 -0
  51. omnibase_infra/nodes/node_contract_persistence_effect/models/model_persistence_result.py +52 -0
  52. omnibase_infra/nodes/node_contract_persistence_effect/node.py +131 -0
  53. omnibase_infra/nodes/node_contract_persistence_effect/registry/__init__.py +27 -0
  54. omnibase_infra/nodes/node_contract_persistence_effect/registry/registry_infra_contract_persistence_effect.py +251 -0
  55. omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +8 -12
  56. omnibase_infra/nodes/node_registry_effect/models/__init__.py +2 -2
  57. omnibase_infra/nodes/node_slack_alerter_effect/__init__.py +33 -0
  58. omnibase_infra/nodes/node_slack_alerter_effect/contract.yaml +291 -0
  59. omnibase_infra/nodes/node_slack_alerter_effect/node.py +106 -0
  60. omnibase_infra/projectors/__init__.py +6 -0
  61. omnibase_infra/projectors/projection_reader_contract.py +1301 -0
  62. omnibase_infra/runtime/__init__.py +12 -0
  63. omnibase_infra/runtime/baseline_subscriptions.py +13 -6
  64. omnibase_infra/runtime/contract_dependency_resolver.py +455 -0
  65. omnibase_infra/runtime/contract_registration_event_router.py +500 -0
  66. omnibase_infra/runtime/db/__init__.py +4 -0
  67. omnibase_infra/runtime/db/models/__init__.py +15 -10
  68. omnibase_infra/runtime/db/models/model_db_operation.py +40 -0
  69. omnibase_infra/runtime/db/models/model_db_param.py +24 -0
  70. omnibase_infra/runtime/db/models/model_db_repository_contract.py +40 -0
  71. omnibase_infra/runtime/db/models/model_db_return.py +26 -0
  72. omnibase_infra/runtime/db/models/model_db_safety_policy.py +32 -0
  73. omnibase_infra/runtime/emit_daemon/event_registry.py +34 -22
  74. omnibase_infra/runtime/event_bus_subcontract_wiring.py +63 -23
  75. omnibase_infra/runtime/intent_execution_router.py +430 -0
  76. omnibase_infra/runtime/models/__init__.py +6 -0
  77. omnibase_infra/runtime/models/model_contract_registry_config.py +41 -0
  78. omnibase_infra/runtime/models/model_intent_execution_summary.py +79 -0
  79. omnibase_infra/runtime/models/model_runtime_config.py +8 -0
  80. omnibase_infra/runtime/protocols/__init__.py +16 -0
  81. omnibase_infra/runtime/protocols/protocol_intent_executor.py +107 -0
  82. omnibase_infra/runtime/publisher_topic_scoped.py +16 -11
  83. omnibase_infra/runtime/registry_policy.py +29 -15
  84. omnibase_infra/runtime/request_response_wiring.py +793 -0
  85. omnibase_infra/runtime/service_kernel.py +295 -8
  86. omnibase_infra/runtime/service_runtime_host_process.py +149 -5
  87. omnibase_infra/runtime/util_version.py +5 -1
  88. omnibase_infra/schemas/schema_latency_baseline.sql +135 -0
  89. omnibase_infra/services/contract_publisher/config.py +4 -4
  90. omnibase_infra/services/contract_publisher/service.py +8 -5
  91. omnibase_infra/services/observability/injection_effectiveness/__init__.py +67 -0
  92. omnibase_infra/services/observability/injection_effectiveness/config.py +295 -0
  93. omnibase_infra/services/observability/injection_effectiveness/consumer.py +1461 -0
  94. omnibase_infra/services/observability/injection_effectiveness/models/__init__.py +32 -0
  95. omnibase_infra/services/observability/injection_effectiveness/models/model_agent_match.py +79 -0
  96. omnibase_infra/services/observability/injection_effectiveness/models/model_context_utilization.py +118 -0
  97. omnibase_infra/services/observability/injection_effectiveness/models/model_latency_breakdown.py +107 -0
  98. omnibase_infra/services/observability/injection_effectiveness/models/model_pattern_utilization.py +46 -0
  99. omnibase_infra/services/observability/injection_effectiveness/writer_postgres.py +596 -0
  100. omnibase_infra/services/registry_api/models/__init__.py +25 -0
  101. omnibase_infra/services/registry_api/models/model_contract_ref.py +44 -0
  102. omnibase_infra/services/registry_api/models/model_contract_view.py +81 -0
  103. omnibase_infra/services/registry_api/models/model_response_contracts.py +50 -0
  104. omnibase_infra/services/registry_api/models/model_response_topics.py +50 -0
  105. omnibase_infra/services/registry_api/models/model_topic_summary.py +57 -0
  106. omnibase_infra/services/registry_api/models/model_topic_view.py +63 -0
  107. omnibase_infra/services/registry_api/routes.py +205 -6
  108. omnibase_infra/services/registry_api/service.py +528 -1
  109. omnibase_infra/utils/__init__.py +7 -0
  110. omnibase_infra/utils/util_db_error_context.py +292 -0
  111. omnibase_infra/validation/infra_validators.py +3 -1
  112. omnibase_infra/validation/validation_exemptions.yaml +65 -0
  113. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/METADATA +3 -3
  114. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/RECORD +117 -58
  115. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/WHEEL +0 -0
  116. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/entry_points.txt +0 -0
  117. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,135 @@
1
+ -- Materialized View: latency_baseline
2
+ -- Description: Hourly latency baseline for A/B comparison (OMN-1890)
3
+ -- Created: 2026-02-04
4
+ --
5
+ -- Purpose: Pre-computes hourly latency statistics per cohort for dashboard queries.
6
+ -- Refreshed hourly (not daily) to catch intra-day drift. Dashboard queries use
7
+ -- COALESCE with 7-day rolling fallback when sample_count is insufficient.
8
+ --
9
+ -- Refresh Strategy:
10
+ -- - Scheduled via pg_cron: REFRESH MATERIALIZED VIEW CONCURRENTLY latency_baseline;
11
+ -- - Recommended schedule: Every hour at :05 (e.g., 00:05, 01:05, ...)
12
+ -- - CONCURRENTLY allows queries during refresh (requires UNIQUE index)
13
+ --
14
+ -- Usage in Dashboard Queries:
15
+ -- SELECT
16
+ -- ie.session_id,
17
+ -- ie.cohort,
18
+ -- ie.user_visible_latency_ms,
19
+ -- ie.user_visible_latency_ms - COALESCE(
20
+ -- lb.avg_latency_ms,
21
+ -- (SELECT AVG(avg_latency_ms) FROM latency_baseline
22
+ -- WHERE cohort = ie.cohort AND hour > NOW() - INTERVAL '7 days')
23
+ -- ) AS latency_delta_ms
24
+ -- FROM injection_effectiveness ie
25
+ -- LEFT JOIN latency_baseline lb
26
+ -- ON DATE_TRUNC('hour', ie.created_at) = lb.hour
27
+ -- AND ie.cohort = lb.cohort;
28
+ --
29
+ -- Related Tickets:
30
+ -- - OMN-1890: Store injection metrics with corrected schema
31
+
32
+ -- ============================================================================
33
+ -- MATERIALIZED VIEW
34
+ -- ============================================================================
35
+
36
+ CREATE MATERIALIZED VIEW IF NOT EXISTS latency_baseline AS
37
+ SELECT
38
+ DATE_TRUNC('hour', created_at) AS hour,
39
+ cohort,
40
+ AVG(user_visible_latency_ms) AS avg_latency_ms,
41
+ PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY user_visible_latency_ms) AS p50_latency_ms,
42
+ PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY user_visible_latency_ms) AS p95_latency_ms,
43
+ PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY user_visible_latency_ms) AS p99_latency_ms,
44
+ MIN(user_visible_latency_ms) AS min_latency_ms,
45
+ MAX(user_visible_latency_ms) AS max_latency_ms,
46
+ STDDEV(user_visible_latency_ms) AS stddev_latency_ms,
47
+ COUNT(*) AS sample_count
48
+ FROM injection_effectiveness
49
+ WHERE user_visible_latency_ms IS NOT NULL
50
+ AND cohort IS NOT NULL
51
+ GROUP BY DATE_TRUNC('hour', created_at), cohort;
52
+
53
+ -- ============================================================================
54
+ -- INDEXES FOR MATERIALIZED VIEW
55
+ -- ============================================================================
56
+
57
+ -- Required for REFRESH CONCURRENTLY
58
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_latency_baseline_hour_cohort
59
+ ON latency_baseline (hour, cohort);
60
+
61
+ -- Fast lookups by cohort
62
+ CREATE INDEX IF NOT EXISTS idx_latency_baseline_cohort
63
+ ON latency_baseline (cohort);
64
+
65
+ -- Recent hour queries
66
+ CREATE INDEX IF NOT EXISTS idx_latency_baseline_hour_desc
67
+ ON latency_baseline (hour DESC);
68
+
69
+ -- ============================================================================
70
+ -- COMMENTS
71
+ -- ============================================================================
72
+
73
+ COMMENT ON MATERIALIZED VIEW latency_baseline IS
74
+ 'Hourly latency baseline per cohort for A/B testing comparison (OMN-1890). '
75
+ 'Refresh hourly via pg_cron. Use COALESCE with 7-day rolling fallback for '
76
+ 'hours with insufficient sample_count.';
77
+
78
+ COMMENT ON COLUMN latency_baseline.hour IS
79
+ 'Truncated hour timestamp for aggregation';
80
+ COMMENT ON COLUMN latency_baseline.cohort IS
81
+ 'A/B test cohort: control or treatment';
82
+ COMMENT ON COLUMN latency_baseline.avg_latency_ms IS
83
+ 'Average user-visible latency in milliseconds';
84
+ COMMENT ON COLUMN latency_baseline.p50_latency_ms IS
85
+ 'Median (50th percentile) latency';
86
+ COMMENT ON COLUMN latency_baseline.p95_latency_ms IS
87
+ '95th percentile latency for tail analysis';
88
+ COMMENT ON COLUMN latency_baseline.p99_latency_ms IS
89
+ '99th percentile latency for extreme tail analysis';
90
+ COMMENT ON COLUMN latency_baseline.sample_count IS
91
+ 'Number of sessions in this hour/cohort. Check for reliability (recommended N>=20).';
92
+
93
+ -- ============================================================================
94
+ -- HELPER FUNCTION: Refresh with logging
95
+ -- ============================================================================
96
+
97
+ -- Note: REFRESH MATERIALIZED VIEW CONCURRENTLY cannot be used inside PL/pgSQL
98
+ -- functions due to transaction context restrictions. Use regular REFRESH here.
99
+ -- For concurrent refresh (allowing queries during refresh), call directly via
100
+ -- pg_cron: REFRESH MATERIALIZED VIEW CONCURRENTLY latency_baseline;
101
+
102
+ CREATE OR REPLACE FUNCTION refresh_latency_baseline()
103
+ RETURNS void AS $$
104
+ DECLARE
105
+ start_time TIMESTAMPTZ;
106
+ end_time TIMESTAMPTZ;
107
+ duration_ms NUMERIC;
108
+ row_count INTEGER;
109
+ BEGIN
110
+ start_time := clock_timestamp();
111
+
112
+ -- Use regular REFRESH (not CONCURRENTLY) inside PL/pgSQL function.
113
+ -- CONCURRENTLY is not allowed inside PL/pgSQL transaction context.
114
+ REFRESH MATERIALIZED VIEW latency_baseline;
115
+
116
+ end_time := clock_timestamp();
117
+
118
+ -- Get row count for logging
119
+ SELECT COUNT(*) INTO row_count FROM latency_baseline;
120
+
121
+ -- Calculate total duration in milliseconds using EPOCH (handles any duration).
122
+ -- Note: EXTRACT(MILLISECONDS FROM interval) only returns the ms component,
123
+ -- not total ms. EXTRACT(EPOCH FROM ...) returns total seconds as decimal.
124
+ duration_ms := EXTRACT(EPOCH FROM (end_time - start_time)) * 1000;
125
+
126
+ RAISE NOTICE 'latency_baseline refreshed: % rows in % ms',
127
+ row_count,
128
+ ROUND(duration_ms)::INTEGER;
129
+ END;
130
+ $$ LANGUAGE plpgsql;
131
+
132
+ COMMENT ON FUNCTION refresh_latency_baseline() IS
133
+ 'Refresh latency_baseline materialized view with timing log. '
134
+ 'Uses regular REFRESH (not CONCURRENTLY) due to PL/pgSQL transaction restrictions. '
135
+ 'For concurrent refresh, call directly: REFRESH MATERIALIZED VIEW CONCURRENTLY latency_baseline;';
@@ -58,7 +58,7 @@ class ModelContractPublisherConfig(BaseModel):
58
58
  package_module: Module name for package resource discovery
59
59
  fail_fast: If True, raise immediately on infrastructure errors
60
60
  allow_zero_contracts: If True, allow empty publish results
61
- environment: Environment prefix for topics (defaults via resolve_environment)
61
+ environment: Environment identifier (used for consumer groups, not topic naming)
62
62
 
63
63
  Example:
64
64
  >>> config = ModelContractPublisherConfig(
@@ -67,8 +67,8 @@ class ModelContractPublisherConfig(BaseModel):
67
67
  ... fail_fast=True,
68
68
  ... allow_zero_contracts=False,
69
69
  ... )
70
- >>> env = config.resolve_environment()
71
- >>> print(f"Publishing to {env}.onex.evt.contract-registered.v1")
70
+ >>> # Topics are realm-agnostic (no environment prefix)
71
+ >>> print("Publishing to onex.evt.contract-registered.v1")
72
72
 
73
73
  .. versionadded:: 0.3.0
74
74
  """
@@ -96,7 +96,7 @@ class ModelContractPublisherConfig(BaseModel):
96
96
  )
97
97
  environment: str | None = Field(
98
98
  default=None,
99
- description="Environment prefix for topics (resolved via resolve_environment)",
99
+ description="Environment identifier for consumer groups (resolved via resolve_environment)",
100
100
  )
101
101
 
102
102
  @model_validator(mode="after")
@@ -26,7 +26,6 @@ from uuid import uuid4
26
26
  import yaml
27
27
  from pydantic import ValidationError
28
28
 
29
- from omnibase_core.constants import TOPIC_SUFFIX_CONTRACT_REGISTERED
30
29
  from omnibase_core.models.contracts.model_handler_contract import ModelHandlerContract
31
30
  from omnibase_core.models.events import ModelContractRegisteredEvent
32
31
  from omnibase_core.protocols.event_bus import ProtocolEventBusPublisher
@@ -35,6 +34,9 @@ from omnibase_infra.errors import (
35
34
  InfraTimeoutError,
36
35
  InfraUnavailableError,
37
36
  )
37
+ from omnibase_infra.runtime.contract_registration_event_router import (
38
+ TOPIC_SUFFIX_CONTRACT_REGISTERED,
39
+ )
38
40
  from omnibase_infra.services.contract_publisher.config import (
39
41
  ModelContractPublisherConfig,
40
42
  )
@@ -214,17 +216,18 @@ class ServiceContractPublisher:
214
216
  )
215
217
 
216
218
  def resolve_topic(self, topic_suffix: str) -> str:
217
- """Resolve topic suffix to full topic name with environment prefix.
219
+ """Resolve topic suffix to topic name (realm-agnostic, no environment prefix).
218
220
 
219
- Uses the same pattern as EventBusSubcontractWiring.
221
+ Topics are realm-agnostic in ONEX. The environment/realm is enforced via
222
+ envelope identity, not topic naming.
220
223
 
221
224
  Args:
222
225
  topic_suffix: Topic suffix (e.g., "onex.evt.contract-registered.v1")
223
226
 
224
227
  Returns:
225
- Full topic name (e.g., "dev.onex.evt.contract-registered.v1")
228
+ Topic name (same as suffix, no environment prefix)
226
229
  """
227
- return f"{self._environment}.{topic_suffix}"
230
+ return topic_suffix
228
231
 
229
232
  async def publish_all(self) -> ModelPublishResult:
230
233
  """Discover and publish all contracts from configured source.
@@ -0,0 +1,67 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Injection Effectiveness Observability Service.
4
+
5
+ This module provides Kafka consumers and PostgreSQL writers for injection
6
+ effectiveness metrics collected from omniclaude hooks.
7
+
8
+ Topics consumed:
9
+ - onex.evt.omniclaude.context-utilization.v1
10
+ - onex.evt.omniclaude.agent-match.v1
11
+ - onex.evt.omniclaude.latency-breakdown.v1
12
+
13
+ Related Tickets:
14
+ - OMN-1890: Store injection metrics with corrected schema
15
+ - OMN-1889: Emit injection metrics + utilization signal (producer)
16
+
17
+ Example:
18
+ >>> from omnibase_infra.services.observability.injection_effectiveness import (
19
+ ... InjectionEffectivenessConsumer,
20
+ ... ConfigInjectionEffectivenessConsumer,
21
+ ... )
22
+ >>>
23
+ >>> config = ConfigInjectionEffectivenessConsumer(
24
+ ... kafka_bootstrap_servers="localhost:9092",
25
+ ... postgres_dsn="postgresql://postgres:secret@localhost:5432/omninode_bridge",
26
+ ... )
27
+ >>> consumer = InjectionEffectivenessConsumer(config)
28
+ >>>
29
+ >>> await consumer.start()
30
+ >>> await consumer.run()
31
+ """
32
+
33
+ from omnibase_infra.services.observability.injection_effectiveness.config import (
34
+ ConfigInjectionEffectivenessConsumer,
35
+ )
36
+ from omnibase_infra.services.observability.injection_effectiveness.consumer import (
37
+ TOPIC_TO_MODEL,
38
+ TOPIC_TO_WRITER_METHOD,
39
+ ConsumerMetrics,
40
+ EnumHealthStatus,
41
+ InjectionEffectivenessConsumer,
42
+ mask_dsn_password,
43
+ )
44
+ from omnibase_infra.services.observability.injection_effectiveness.models import (
45
+ ModelAgentMatchEvent,
46
+ ModelContextUtilizationEvent,
47
+ ModelLatencyBreakdownEvent,
48
+ ModelPatternUtilization,
49
+ )
50
+ from omnibase_infra.services.observability.injection_effectiveness.writer_postgres import (
51
+ WriterInjectionEffectivenessPostgres,
52
+ )
53
+
54
+ __all__ = [
55
+ "ConfigInjectionEffectivenessConsumer",
56
+ "ConsumerMetrics",
57
+ "EnumHealthStatus",
58
+ "InjectionEffectivenessConsumer",
59
+ "ModelAgentMatchEvent",
60
+ "ModelContextUtilizationEvent",
61
+ "ModelLatencyBreakdownEvent",
62
+ "ModelPatternUtilization",
63
+ "TOPIC_TO_MODEL",
64
+ "TOPIC_TO_WRITER_METHOD",
65
+ "WriterInjectionEffectivenessPostgres",
66
+ "mask_dsn_password",
67
+ ]
@@ -0,0 +1,295 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Configuration for injection effectiveness observability consumer.
4
+
5
+ This module provides Pydantic Settings configuration for the injection
6
+ effectiveness Kafka consumer service. Configuration is loaded from environment
7
+ variables with the ``OMNIBASE_INFRA_INJECTION_EFFECTIVENESS_`` prefix.
8
+
9
+ Configuration Groups:
10
+ - **Kafka**: Bootstrap servers, consumer group, topics, auto-offset reset
11
+ - **PostgreSQL**: DSN connection string, pool sizing
12
+ - **Batch Processing**: Batch size, timeout, poll buffer
13
+ - **Circuit Breaker**: Threshold, reset timeout, half-open successes
14
+ - **Health Check**: Port, host, staleness thresholds, startup grace period
15
+ - **Pattern Analytics**: Minimum support threshold for statistical confidence
16
+
17
+ Environment Variables:
18
+ All configuration values can be set via environment variables with the
19
+ ``OMNIBASE_INFRA_INJECTION_EFFECTIVENESS_`` prefix. For example:
20
+
21
+ - ``OMNIBASE_INFRA_INJECTION_EFFECTIVENESS_KAFKA_BOOTSTRAP_SERVERS``
22
+ - ``OMNIBASE_INFRA_INJECTION_EFFECTIVENESS_POSTGRES_DSN``
23
+ - ``OMNIBASE_INFRA_INJECTION_EFFECTIVENESS_BATCH_SIZE``
24
+ - ``OMNIBASE_INFRA_INJECTION_EFFECTIVENESS_CIRCUIT_BREAKER_THRESHOLD``
25
+
26
+ Validation:
27
+ The configuration validates:
28
+ - At least one topic must be configured
29
+ - Pool min size must be <= pool max size
30
+ - Timing relationships (warns if circuit breaker timeout < 2x batch timeout)
31
+
32
+ Related Tickets:
33
+ - OMN-1890: Store injection metrics with corrected schema
34
+ - OMN-1889: Emit injection metrics from omniclaude hooks (producer)
35
+
36
+ Example:
37
+ >>> from omnibase_infra.services.observability.injection_effectiveness.config import (
38
+ ... ConfigInjectionEffectivenessConsumer,
39
+ ... )
40
+ >>>
41
+ >>> # Load from environment (default)
42
+ >>> config = ConfigInjectionEffectivenessConsumer()
43
+ >>>
44
+ >>> # Or with explicit values
45
+ >>> config = ConfigInjectionEffectivenessConsumer(
46
+ ... kafka_bootstrap_servers="kafka.example.com:9092",
47
+ ... postgres_dsn="postgresql://user:pass@host:5432/db",
48
+ ... batch_size=200,
49
+ ... )
50
+ >>>
51
+ >>> print(config.topics)
52
+ ['onex.evt.omniclaude.context-utilization.v1', ...]
53
+ """
54
+
55
+ from __future__ import annotations
56
+
57
+ import logging
58
+ from typing import Self
59
+
60
+ from pydantic import Field, model_validator
61
+ from pydantic_settings import BaseSettings, SettingsConfigDict
62
+
63
+ from omnibase_infra.enums import EnumInfraTransportType
64
+ from omnibase_infra.errors import ModelInfraErrorContext, ProtocolConfigurationError
65
+
66
+ logger = logging.getLogger(__name__)
67
+
68
+
69
+ class ConfigInjectionEffectivenessConsumer(BaseSettings):
70
+ """Configuration for the injection effectiveness Kafka consumer.
71
+
72
+ Environment variables use the OMNIBASE_INFRA_INJECTION_EFFECTIVENESS_ prefix.
73
+ Example: OMNIBASE_INFRA_INJECTION_EFFECTIVENESS_KAFKA_BOOTSTRAP_SERVERS=kafka.example.com:9092
74
+
75
+ This consumer subscribes to injection effectiveness topics and
76
+ persists events to PostgreSQL for A/B testing analytics.
77
+ """
78
+
79
+ model_config = SettingsConfigDict(
80
+ env_prefix="OMNIBASE_INFRA_INJECTION_EFFECTIVENESS_",
81
+ env_file=".env",
82
+ env_file_encoding="utf-8",
83
+ case_sensitive=False,
84
+ extra="ignore",
85
+ )
86
+
87
+ # Kafka connection
88
+ kafka_bootstrap_servers: str = Field(
89
+ default="localhost:9092",
90
+ description=(
91
+ "Kafka bootstrap servers. Set via "
92
+ "OMNIBASE_INFRA_INJECTION_EFFECTIVENESS_KAFKA_BOOTSTRAP_SERVERS env var."
93
+ ),
94
+ )
95
+ kafka_group_id: str = Field(
96
+ default="injection-effectiveness-postgres",
97
+ description="Consumer group ID for offset tracking",
98
+ )
99
+
100
+ # Topics to subscribe (3 injection effectiveness topics from OMN-1889)
101
+ topics: list[str] = Field(
102
+ default_factory=lambda: [
103
+ "onex.evt.omniclaude.context-utilization.v1",
104
+ "onex.evt.omniclaude.agent-match.v1",
105
+ "onex.evt.omniclaude.latency-breakdown.v1",
106
+ ],
107
+ description="Kafka topics to consume for injection effectiveness",
108
+ )
109
+
110
+ # Consumer behavior
111
+ auto_offset_reset: str = Field(
112
+ default="earliest",
113
+ description="Where to start consuming if no offset exists",
114
+ )
115
+ enable_auto_commit: bool = Field(
116
+ default=False,
117
+ description="Disable auto-commit for at-least-once delivery",
118
+ )
119
+
120
+ # PostgreSQL connection
121
+ postgres_dsn: str = Field(
122
+ description=(
123
+ "PostgreSQL connection string. Set via "
124
+ "OMNIBASE_INFRA_INJECTION_EFFECTIVENESS_POSTGRES_DSN env var."
125
+ ),
126
+ )
127
+
128
+ # Batch processing
129
+ batch_size: int = Field(
130
+ default=100,
131
+ ge=1,
132
+ le=1000,
133
+ description="Maximum records per batch write",
134
+ )
135
+ batch_timeout_ms: int = Field(
136
+ default=1000,
137
+ ge=100,
138
+ le=60000,
139
+ description="Timeout for batch accumulation in milliseconds",
140
+ )
141
+ poll_timeout_buffer_seconds: float = Field(
142
+ default=5.0,
143
+ ge=1.0,
144
+ le=30.0,
145
+ description=(
146
+ "Additional buffer time added to batch_timeout_ms for asyncio.wait_for."
147
+ ),
148
+ )
149
+
150
+ # Circuit breaker
151
+ circuit_breaker_threshold: int = Field(
152
+ default=5,
153
+ ge=1,
154
+ le=100,
155
+ description="Failures before circuit opens",
156
+ )
157
+ circuit_breaker_reset_timeout: float = Field(
158
+ default=60.0,
159
+ ge=1.0,
160
+ le=3600.0,
161
+ description="Seconds before circuit half-opens for retry",
162
+ )
163
+ circuit_breaker_half_open_successes: int = Field(
164
+ default=1,
165
+ ge=1,
166
+ le=10,
167
+ description="Successful requests required to close circuit from half-open state",
168
+ )
169
+
170
+ # Minimum support gating for pattern confidence (R3 requirement)
171
+ min_pattern_support: int = Field(
172
+ default=20,
173
+ ge=1,
174
+ le=1000,
175
+ description=(
176
+ "Minimum number of sessions required before pattern utilization "
177
+ "metrics are considered statistically reliable (N=20 default)."
178
+ ),
179
+ )
180
+
181
+ # PostgreSQL pool settings
182
+ pool_min_size: int = Field(
183
+ default=2,
184
+ ge=1,
185
+ le=20,
186
+ description="Minimum number of connections in the PostgreSQL connection pool.",
187
+ )
188
+ pool_max_size: int = Field(
189
+ default=10,
190
+ ge=2,
191
+ le=100,
192
+ description="Maximum number of connections in the PostgreSQL connection pool.",
193
+ )
194
+
195
+ # Health check
196
+ health_check_port: int = Field(
197
+ default=8088,
198
+ ge=1024,
199
+ le=65535,
200
+ description="Port for HTTP health check endpoint",
201
+ )
202
+ health_check_host: str = Field(
203
+ default="0.0.0.0", # noqa: S104 - Configurable for container access
204
+ description="Host/IP for health check server binding.",
205
+ )
206
+ health_check_staleness_seconds: int = Field(
207
+ default=300,
208
+ ge=60,
209
+ le=3600,
210
+ description="Maximum age for last successful write before DEGRADED status.",
211
+ )
212
+ health_check_poll_staleness_seconds: int = Field(
213
+ default=60,
214
+ ge=10,
215
+ le=300,
216
+ description="Maximum age for last poll before DEGRADED status.",
217
+ )
218
+ startup_grace_period_seconds: float = Field(
219
+ default=60.0,
220
+ ge=10.0,
221
+ le=300.0,
222
+ description=(
223
+ "Grace period in seconds after startup during which the consumer is "
224
+ "considered healthy even without successful writes. Allows time for "
225
+ "initial Kafka partition assignment and first message processing."
226
+ ),
227
+ )
228
+
229
+ @model_validator(mode="after")
230
+ def validate_topic_configuration(self) -> Self:
231
+ """Ensure topics are configured.
232
+
233
+ Returns:
234
+ Self if validation passes.
235
+
236
+ Raises:
237
+ ProtocolConfigurationError: If no topics are configured.
238
+ """
239
+ if not self.topics:
240
+ # Auto-generate correlation_id for configuration errors
241
+ # (no request context available during model validation)
242
+ context = ModelInfraErrorContext.with_correlation(
243
+ transport_type=EnumInfraTransportType.RUNTIME,
244
+ operation="validate_topic_configuration",
245
+ target_name="ConfigInjectionEffectivenessConsumer",
246
+ )
247
+ raise ProtocolConfigurationError(
248
+ "No topics configured for injection effectiveness consumer.",
249
+ context=context,
250
+ )
251
+ return self
252
+
253
+ @model_validator(mode="after")
254
+ def validate_timing_relationships(self) -> Self:
255
+ """Validate timing relationships between configuration values.
256
+
257
+ Returns:
258
+ Self if validation passes.
259
+ """
260
+ batch_timeout_seconds = self.batch_timeout_ms / 1000
261
+ min_recommended_circuit_timeout = batch_timeout_seconds * 2
262
+
263
+ if self.circuit_breaker_reset_timeout < min_recommended_circuit_timeout:
264
+ logger.warning(
265
+ "Circuit breaker timeout (%.1fs) is less than 2x batch timeout (%.1fs).",
266
+ self.circuit_breaker_reset_timeout,
267
+ batch_timeout_seconds,
268
+ )
269
+ return self
270
+
271
+ @model_validator(mode="after")
272
+ def validate_pool_size_relationship(self) -> Self:
273
+ """Validate pool size relationship (min <= max).
274
+
275
+ Returns:
276
+ Self if validation passes.
277
+
278
+ Raises:
279
+ ProtocolConfigurationError: If pool_min_size > pool_max_size.
280
+ """
281
+ if self.pool_min_size > self.pool_max_size:
282
+ context = ModelInfraErrorContext.with_correlation(
283
+ transport_type=EnumInfraTransportType.RUNTIME,
284
+ operation="validate_pool_size_relationship",
285
+ target_name="ConfigInjectionEffectivenessConsumer",
286
+ )
287
+ raise ProtocolConfigurationError(
288
+ f"pool_min_size ({self.pool_min_size}) must be <= pool_max_size "
289
+ f"({self.pool_max_size}).",
290
+ context=context,
291
+ )
292
+ return self
293
+
294
+
295
+ __all__ = ["ConfigInjectionEffectivenessConsumer"]