agentflow-runtime 1.1.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 (100) hide show
  1. agentflow_runtime-1.1.0.dist-info/METADATA +55 -0
  2. agentflow_runtime-1.1.0.dist-info/RECORD +100 -0
  3. agentflow_runtime-1.1.0.dist-info/WHEEL +4 -0
  4. agentflow_runtime-1.1.0.dist-info/licenses/LICENSE +21 -0
  5. src/__init__.py +0 -0
  6. src/constants.py +3 -0
  7. src/ingestion/__init__.py +0 -0
  8. src/ingestion/cdc/__init__.py +5 -0
  9. src/ingestion/cdc/normalizer.py +186 -0
  10. src/ingestion/connectors/__init__.py +0 -0
  11. src/ingestion/connectors/mysql_cdc.py +63 -0
  12. src/ingestion/connectors/postgres_cdc.py +68 -0
  13. src/ingestion/producers/__init__.py +0 -0
  14. src/ingestion/producers/event_producer.py +237 -0
  15. src/ingestion/schemas/__init__.py +0 -0
  16. src/ingestion/schemas/events.py +147 -0
  17. src/ingestion/tenant_router.py +80 -0
  18. src/logger.py +41 -0
  19. src/orchestration/__init__.py +0 -0
  20. src/orchestration/dags/__init__.py +0 -0
  21. src/orchestration/dags/daily_batch.py +201 -0
  22. src/processing/__init__.py +0 -0
  23. src/processing/event_replayer.py +250 -0
  24. src/processing/flink_jobs/Dockerfile +55 -0
  25. src/processing/flink_jobs/__init__.py +0 -0
  26. src/processing/flink_jobs/checkpointing.py +32 -0
  27. src/processing/flink_jobs/session_aggregation.py +212 -0
  28. src/processing/flink_jobs/session_aggregator.py +199 -0
  29. src/processing/flink_jobs/stream_processor.py +316 -0
  30. src/processing/iceberg_sink.py +348 -0
  31. src/processing/local_pipeline.py +452 -0
  32. src/processing/outbox.py +273 -0
  33. src/processing/tracing.py +36 -0
  34. src/processing/transformations/__init__.py +0 -0
  35. src/processing/transformations/enrichment.py +125 -0
  36. src/quality/__init__.py +0 -0
  37. src/quality/monitors/__init__.py +0 -0
  38. src/quality/monitors/freshness_monitor.py +166 -0
  39. src/quality/monitors/metrics_collector.py +367 -0
  40. src/quality/validators/__init__.py +0 -0
  41. src/quality/validators/schema_validator.py +119 -0
  42. src/quality/validators/semantic_validator.py +202 -0
  43. src/serving/__init__.py +0 -0
  44. src/serving/api/__init__.py +0 -0
  45. src/serving/api/alert_dispatcher.py +51 -0
  46. src/serving/api/alerts/__init__.py +38 -0
  47. src/serving/api/alerts/dispatcher.py +299 -0
  48. src/serving/api/alerts/escalation.py +290 -0
  49. src/serving/api/alerts/evaluator.py +81 -0
  50. src/serving/api/alerts/history.py +115 -0
  51. src/serving/api/analytics.py +543 -0
  52. src/serving/api/auth/__init__.py +46 -0
  53. src/serving/api/auth/key_rotation.py +400 -0
  54. src/serving/api/auth/manager.py +406 -0
  55. src/serving/api/auth/middleware.py +331 -0
  56. src/serving/api/main.py +390 -0
  57. src/serving/api/middleware/logging.py +41 -0
  58. src/serving/api/middleware/tracing.py +51 -0
  59. src/serving/api/rate_limiter.py +76 -0
  60. src/serving/api/routers/__init__.py +0 -0
  61. src/serving/api/routers/admin.py +150 -0
  62. src/serving/api/routers/admin_ui.py +93 -0
  63. src/serving/api/routers/agent_query.py +639 -0
  64. src/serving/api/routers/alerts.py +134 -0
  65. src/serving/api/routers/batch.py +231 -0
  66. src/serving/api/routers/contracts.py +98 -0
  67. src/serving/api/routers/deadletter.py +337 -0
  68. src/serving/api/routers/lineage.py +218 -0
  69. src/serving/api/routers/search.py +103 -0
  70. src/serving/api/routers/slo.py +231 -0
  71. src/serving/api/routers/stream.py +141 -0
  72. src/serving/api/routers/webhooks.py +93 -0
  73. src/serving/api/security.py +83 -0
  74. src/serving/api/telemetry.py +66 -0
  75. src/serving/api/templates/admin.html +214 -0
  76. src/serving/api/versioning.py +328 -0
  77. src/serving/api/webhook_dispatcher.py +423 -0
  78. src/serving/backends/__init__.py +117 -0
  79. src/serving/backends/clickhouse_backend.py +310 -0
  80. src/serving/backends/duckdb_backend.py +268 -0
  81. src/serving/cache.py +169 -0
  82. src/serving/db_pool.py +105 -0
  83. src/serving/masking.py +122 -0
  84. src/serving/semantic_layer/__init__.py +0 -0
  85. src/serving/semantic_layer/catalog.py +177 -0
  86. src/serving/semantic_layer/contract_registry.py +258 -0
  87. src/serving/semantic_layer/entity_type_registry.py +107 -0
  88. src/serving/semantic_layer/nl_engine.py +189 -0
  89. src/serving/semantic_layer/query/__init__.py +3 -0
  90. src/serving/semantic_layer/query/contracts.py +47 -0
  91. src/serving/semantic_layer/query/engine.py +81 -0
  92. src/serving/semantic_layer/query/entity_queries.py +221 -0
  93. src/serving/semantic_layer/query/metric_queries.py +84 -0
  94. src/serving/semantic_layer/query/nl_queries.py +305 -0
  95. src/serving/semantic_layer/query/sql_builder.py +113 -0
  96. src/serving/semantic_layer/query/sql_guard.py +3 -0
  97. src/serving/semantic_layer/query_engine.py +5 -0
  98. src/serving/semantic_layer/schema_evolution.py +175 -0
  99. src/serving/semantic_layer/search_index.py +337 -0
  100. src/serving/semantic_layer/sql_guard.py +56 -0
@@ -0,0 +1,237 @@
1
+ """Simulates realistic e-commerce event streams for local development.
2
+
3
+ Produces orders, payments, clickstream, and product events to Kafka topics
4
+ with configurable throughput. Events follow realistic distributions:
5
+ - Orders follow a daily pattern (peaks at 10am, 2pm, 8pm)
6
+ - Payments follow orders with 0-5s delay
7
+ - Clickstream is 10x order volume
8
+ - Products update in batches every ~60s
9
+ """
10
+
11
+ import json
12
+ import random
13
+ import time
14
+ import uuid
15
+ from datetime import UTC, datetime
16
+ from decimal import Decimal
17
+
18
+ import structlog
19
+ from confluent_kafka import Producer
20
+ from pydantic_settings import BaseSettings
21
+
22
+ from src.ingestion.schemas.events import (
23
+ ClickstreamEvent,
24
+ Currency,
25
+ EventType,
26
+ OrderEvent,
27
+ OrderItem,
28
+ OrderStatus,
29
+ PaymentEvent,
30
+ PaymentMethod,
31
+ ProductEvent,
32
+ )
33
+
34
+ logger = structlog.get_logger()
35
+
36
+
37
+ class ProducerConfig(BaseSettings):
38
+ kafka_bootstrap_servers: str = "localhost:9092"
39
+ events_per_second: int = 100
40
+ order_ratio: float = 0.15
41
+ payment_ratio: float = 0.10
42
+ click_ratio: float = 0.70
43
+ product_ratio: float = 0.05
44
+
45
+ model_config = {"env_prefix": "PRODUCER_"}
46
+
47
+
48
+ PRODUCT_CATALOG = [
49
+ ("PROD-001", "Wireless Headphones", "electronics", Decimal("79.99")),
50
+ ("PROD-002", "Running Shoes", "footwear", Decimal("129.99")),
51
+ ("PROD-003", "Coffee Maker", "kitchen", Decimal("49.99")),
52
+ ("PROD-004", "Mechanical Keyboard", "electronics", Decimal("149.99")),
53
+ ("PROD-005", "Yoga Mat", "fitness", Decimal("34.99")),
54
+ ("PROD-006", "Backpack", "accessories", Decimal("89.99")),
55
+ ("PROD-007", "Water Bottle", "fitness", Decimal("24.99")),
56
+ ("PROD-008", "Desk Lamp", "home", Decimal("44.99")),
57
+ ("PROD-009", "Bluetooth Speaker", "electronics", Decimal("59.99")),
58
+ ("PROD-010", "Sunglasses", "accessories", Decimal("119.99")),
59
+ ]
60
+
61
+ PAGES = ["/", "/products", "/cart", "/checkout", "/account", "/search"]
62
+ USER_AGENTS = [
63
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
64
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
65
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)",
66
+ "Mozilla/5.0 (Linux; Android 14; Pixel 8)",
67
+ ]
68
+
69
+
70
+ class DecimalEncoder(json.JSONEncoder):
71
+ def default(self, o):
72
+ if isinstance(o, Decimal):
73
+ return float(o)
74
+ return super().default(o)
75
+
76
+
77
+ def _delivery_report(err, msg):
78
+ if err:
79
+ logger.error("delivery_failed", error=str(err), topic=msg.topic())
80
+
81
+
82
+ def _now() -> datetime:
83
+ return datetime.now(UTC)
84
+
85
+
86
+ def _uuid() -> str:
87
+ return str(uuid.uuid4())
88
+
89
+
90
+ def generate_order() -> tuple[str, OrderEvent]:
91
+ products = random.sample(PRODUCT_CATALOG, k=random.randint(1, 4))
92
+ items = [
93
+ OrderItem(
94
+ product_id=p[0],
95
+ quantity=random.randint(1, 3),
96
+ unit_price=p[3],
97
+ )
98
+ for p in products
99
+ ]
100
+ total = sum((item.quantity * item.unit_price for item in items), Decimal(0))
101
+ order_id = f"ORD-{_now().strftime('%Y%m%d')}-{random.randint(1000, 9999)}"
102
+
103
+ event = OrderEvent(
104
+ event_id=_uuid(),
105
+ event_type=EventType.ORDER_CREATED,
106
+ timestamp=_now(),
107
+ source="web-store",
108
+ order_id=order_id,
109
+ user_id=f"USR-{random.randint(10000, 99999)}",
110
+ status=OrderStatus.PENDING,
111
+ items=items,
112
+ total_amount=total,
113
+ currency=Currency.USD,
114
+ )
115
+ return "orders.raw", event
116
+
117
+
118
+ def generate_payment(order_id: str | None = None) -> tuple[str, PaymentEvent]:
119
+ oid = order_id or f"ORD-{_now().strftime('%Y%m%d')}-{random.randint(1000, 9999)}"
120
+ event = PaymentEvent(
121
+ event_id=_uuid(),
122
+ event_type=EventType.PAYMENT_INITIATED,
123
+ timestamp=_now(),
124
+ source="payment-gateway",
125
+ payment_id=f"PAY-{_uuid()[:8]}",
126
+ order_id=oid,
127
+ user_id=f"USR-{random.randint(10000, 99999)}",
128
+ amount=Decimal(str(round(random.uniform(20, 500), 2))),
129
+ currency=Currency.USD,
130
+ method=random.choice(list(PaymentMethod)),
131
+ status="initiated",
132
+ )
133
+ return "payments.raw", event
134
+
135
+
136
+ def generate_click() -> tuple[str, ClickstreamEvent]:
137
+ product = random.choice(PRODUCT_CATALOG) if random.random() > 0.4 else None
138
+ page = f"/products/{product[0]}" if product else random.choice(PAGES)
139
+ event = ClickstreamEvent(
140
+ event_id=_uuid(),
141
+ event_type=random.choice([EventType.CLICK, EventType.PAGE_VIEW, EventType.ADD_TO_CART]),
142
+ timestamp=_now(),
143
+ source="web-tracker",
144
+ session_id=f"SES-{_uuid()[:12]}",
145
+ user_id=f"USR-{random.randint(10000, 99999)}" if random.random() > 0.3 else None,
146
+ page_url=page,
147
+ referrer="https://google.com" if random.random() > 0.5 else None,
148
+ user_agent=random.choice(USER_AGENTS),
149
+ viewport_width=random.choice([375, 768, 1024, 1440, 1920]),
150
+ product_id=product[0] if product else None,
151
+ )
152
+ return "clicks.raw", event
153
+
154
+
155
+ def generate_product() -> tuple[str, ProductEvent]:
156
+ product = random.choice(PRODUCT_CATALOG)
157
+ event = ProductEvent(
158
+ event_id=_uuid(),
159
+ event_type=EventType.PRODUCT_UPDATED,
160
+ timestamp=_now(),
161
+ source="inventory-service",
162
+ product_id=product[0],
163
+ name=product[1],
164
+ category=product[2],
165
+ price=product[3],
166
+ currency=Currency.USD,
167
+ in_stock=random.random() > 0.1,
168
+ stock_quantity=random.randint(0, 500),
169
+ )
170
+ return "products.cdc", event
171
+
172
+
173
+ def run_producer():
174
+ config = ProducerConfig()
175
+ producer = Producer(
176
+ {
177
+ "bootstrap.servers": config.kafka_bootstrap_servers,
178
+ "linger.ms": 50,
179
+ "batch.num.messages": 500,
180
+ "compression.type": "lz4",
181
+ "acks": "all",
182
+ }
183
+ )
184
+
185
+ generators: list[tuple] = [
186
+ (config.order_ratio, generate_order),
187
+ (config.payment_ratio, generate_payment),
188
+ (config.click_ratio, generate_click),
189
+ (config.product_ratio, generate_product),
190
+ ]
191
+
192
+ logger.info(
193
+ "producer_started",
194
+ eps=config.events_per_second,
195
+ bootstrap=config.kafka_bootstrap_servers,
196
+ )
197
+
198
+ produced = 0
199
+ interval = 1.0 / config.events_per_second
200
+
201
+ try:
202
+ while True:
203
+ roll = random.random()
204
+ cumulative = 0.0
205
+
206
+ for ratio, gen in generators:
207
+ cumulative += ratio
208
+ if roll <= cumulative:
209
+ topic, event = gen()
210
+ producer.produce(
211
+ topic,
212
+ key=event.event_id.encode(),
213
+ value=json.dumps(
214
+ event.model_dump(mode="json"),
215
+ cls=DecimalEncoder,
216
+ ).encode(),
217
+ callback=_delivery_report,
218
+ )
219
+ produced += 1
220
+ break
221
+
222
+ if produced % 1000 == 0:
223
+ producer.flush()
224
+ logger.info("producer_progress", total_produced=produced)
225
+
226
+ producer.poll(0)
227
+ time.sleep(interval)
228
+
229
+ except KeyboardInterrupt:
230
+ logger.info("producer_stopping", total_produced=produced)
231
+ finally:
232
+ producer.flush(timeout=10)
233
+ logger.info("producer_stopped", total_produced=produced)
234
+
235
+
236
+ if __name__ == "__main__":
237
+ run_producer()
File without changes
@@ -0,0 +1,147 @@
1
+ """Canonical event schemas for all data sources.
2
+
3
+ These Pydantic models serve as the single source of truth for event structure.
4
+ Used by producers (serialization), Flink jobs (validation), and the API (response types).
5
+ """
6
+
7
+ from datetime import UTC, datetime
8
+ from decimal import Decimal
9
+ from enum import StrEnum
10
+
11
+ from pydantic import BaseModel, Field, field_validator
12
+
13
+
14
+ class EventType(StrEnum):
15
+ ORDER_CREATED = "order.created"
16
+ ORDER_UPDATED = "order.updated"
17
+ ORDER_CANCELLED = "order.cancelled"
18
+ PAYMENT_INITIATED = "payment.initiated"
19
+ PAYMENT_COMPLETED = "payment.completed"
20
+ PAYMENT_FAILED = "payment.failed"
21
+ CLICK = "click"
22
+ PAGE_VIEW = "page_view"
23
+ ADD_TO_CART = "add_to_cart"
24
+ PRODUCT_UPDATED = "product.updated"
25
+
26
+
27
+ class CdcOperation(StrEnum):
28
+ SNAPSHOT = "snapshot"
29
+ INSERT = "insert"
30
+ UPDATE = "update"
31
+ DELETE = "delete"
32
+ DDL = "ddl"
33
+
34
+
35
+ class CdcSource(StrEnum):
36
+ POSTGRES = "postgres_cdc"
37
+ MYSQL = "mysql_cdc"
38
+
39
+
40
+ class Currency(StrEnum):
41
+ USD = "USD"
42
+ EUR = "EUR"
43
+ GBP = "GBP"
44
+
45
+
46
+ class OrderStatus(StrEnum):
47
+ PENDING = "pending"
48
+ CONFIRMED = "confirmed"
49
+ SHIPPED = "shipped"
50
+ DELIVERED = "delivered"
51
+ CANCELLED = "cancelled"
52
+
53
+
54
+ class BaseEvent(BaseModel):
55
+ event_id: str = Field(..., pattern=r"^[a-f0-9\-]{36}$")
56
+ event_type: EventType
57
+ timestamp: datetime
58
+ source: str = Field(..., min_length=1, max_length=64)
59
+
60
+ @field_validator("timestamp")
61
+ @classmethod
62
+ def timestamp_not_future(cls, v: datetime) -> datetime:
63
+ now = datetime.now(UTC)
64
+ if v.tzinfo is None:
65
+ v = v.replace(tzinfo=UTC)
66
+ # Allow 5 minutes of clock skew
67
+ max_future = now.timestamp() + 300
68
+ if v.timestamp() > max_future:
69
+ msg = f"Event timestamp {v} is too far in the future"
70
+ raise ValueError(msg)
71
+ return v
72
+
73
+
74
+ class CdcEvent(BaseModel):
75
+ event_id: str = Field(..., min_length=1)
76
+ event_type: str = Field(..., min_length=1, max_length=128)
77
+ operation: CdcOperation
78
+ timestamp: datetime
79
+ source: CdcSource
80
+ entity_type: str = Field(..., min_length=1, max_length=64)
81
+ entity_id: str = Field(..., min_length=1, max_length=256)
82
+ before: dict | None = None
83
+ after: dict | None = None
84
+ source_metadata: dict = Field(..., min_length=1)
85
+
86
+
87
+ class OrderItem(BaseModel):
88
+ product_id: str
89
+ quantity: int = Field(..., gt=0, le=1000)
90
+ unit_price: Decimal = Field(..., gt=0, decimal_places=2)
91
+
92
+
93
+ class OrderEvent(BaseEvent):
94
+ order_id: str = Field(..., pattern=r"^ORD-\d{8}-\d{4,}$")
95
+ user_id: str
96
+ status: OrderStatus
97
+ items: list[OrderItem] = Field(..., min_length=1)
98
+ total_amount: Decimal = Field(..., gt=0)
99
+ currency: Currency = Currency.USD
100
+
101
+ @field_validator("total_amount")
102
+ @classmethod
103
+ def total_matches_items(cls, v: Decimal, info) -> Decimal:
104
+ items = info.data.get("items")
105
+ if items:
106
+ expected = sum(item.quantity * item.unit_price for item in items)
107
+ if abs(v - expected) > Decimal("0.01"):
108
+ msg = f"Total {v} doesn't match sum of items {expected}"
109
+ raise ValueError(msg)
110
+ return v
111
+
112
+
113
+ class PaymentMethod(StrEnum):
114
+ CARD = "card"
115
+ BANK_TRANSFER = "bank_transfer"
116
+ WALLET = "wallet"
117
+
118
+
119
+ class PaymentEvent(BaseEvent):
120
+ payment_id: str
121
+ order_id: str = Field(..., pattern=r"^ORD-\d{8}-\d{4,}$")
122
+ user_id: str
123
+ amount: Decimal = Field(..., gt=0)
124
+ currency: Currency = Currency.USD
125
+ method: PaymentMethod
126
+ status: str = Field(..., pattern=r"^(initiated|completed|failed|refunded)$")
127
+ failure_reason: str | None = None
128
+
129
+
130
+ class ClickstreamEvent(BaseEvent):
131
+ session_id: str
132
+ user_id: str | None = None # anonymous users allowed
133
+ page_url: str
134
+ referrer: str | None = None
135
+ user_agent: str
136
+ viewport_width: int | None = None
137
+ product_id: str | None = None # set if on a product page
138
+
139
+
140
+ class ProductEvent(BaseEvent):
141
+ product_id: str
142
+ name: str = Field(..., min_length=1, max_length=500)
143
+ category: str
144
+ price: Decimal = Field(..., ge=0)
145
+ currency: Currency = Currency.USD
146
+ in_stock: bool
147
+ stock_quantity: int = Field(..., ge=0)
@@ -0,0 +1,80 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+ try:
10
+ import yaml # type: ignore[import-untyped]
11
+ except ImportError: # pragma: no cover
12
+ yaml = None
13
+
14
+
15
+ def default_tenants_config_path() -> Path:
16
+ return Path(os.getenv("AGENTFLOW_TENANTS_FILE", "config/tenants.yaml"))
17
+
18
+
19
+ class TenantDefinition(BaseModel):
20
+ id: str
21
+ display_name: str
22
+ kafka_topic_prefix: str
23
+ duckdb_schema: str
24
+ max_events_per_day: int
25
+ max_api_keys: int
26
+ allowed_entity_types: list[str] | None = None
27
+
28
+
29
+ class TenantsConfig(BaseModel):
30
+ tenants: list[TenantDefinition] = Field(default_factory=list)
31
+
32
+
33
+ class TenantRouter:
34
+ def __init__(self, config_path: Path | str | None = None) -> None:
35
+ self.config_path = (
36
+ Path(config_path) if config_path is not None else default_tenants_config_path()
37
+ )
38
+ self._has_config: bool | None = None
39
+ self._config: TenantsConfig | None = None
40
+
41
+ def has_config(self) -> bool:
42
+ if self._has_config is None:
43
+ self._has_config = self.config_path.exists()
44
+ return self._has_config
45
+
46
+ def load(self) -> TenantsConfig:
47
+ if self._config is not None:
48
+ return self._config
49
+
50
+ if not self.has_config():
51
+ self._config = TenantsConfig()
52
+ return self._config
53
+
54
+ raw = self.config_path.read_text(encoding="utf-8")
55
+ if yaml is not None:
56
+ data = yaml.safe_load(raw) or {}
57
+ else: # pragma: no cover
58
+ data = json.loads(raw)
59
+ self._config = TenantsConfig.model_validate(data)
60
+ return self._config
61
+
62
+ def get_tenant(self, tenant_id: str | None) -> TenantDefinition | None:
63
+ if tenant_id is None:
64
+ return None
65
+ for tenant in self.load().tenants:
66
+ if tenant.id == tenant_id:
67
+ return tenant
68
+ return None
69
+
70
+ def get_duckdb_schema(self, tenant_id: str | None) -> str | None:
71
+ tenant = self.get_tenant(tenant_id)
72
+ if tenant is None:
73
+ return None
74
+ return tenant.duckdb_schema
75
+
76
+ def route_topic(self, topic: str, tenant_id: str | None) -> str:
77
+ tenant = self.get_tenant(tenant_id)
78
+ if tenant is None:
79
+ return topic
80
+ return f"{tenant.kafka_topic_prefix}.{topic}"
src/logger.py ADDED
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ from collections.abc import MutableMapping
6
+ from typing import Any
7
+
8
+ import structlog
9
+ from opentelemetry import trace
10
+
11
+
12
+ def add_otel_context(
13
+ _logger: Any,
14
+ _method_name: str,
15
+ event_dict: MutableMapping[str, Any],
16
+ ) -> MutableMapping[str, Any]:
17
+ span = trace.get_current_span()
18
+ if not span.is_recording():
19
+ return event_dict
20
+ span_context = span.get_span_context()
21
+ event_dict["trace_id"] = format(span_context.trace_id, "032x")
22
+ event_dict["span_id"] = format(span_context.span_id, "016x")
23
+ return event_dict
24
+
25
+
26
+ def configure_logging() -> None:
27
+ level_name = os.getenv("LOG_LEVEL", "INFO").upper()
28
+ level = getattr(logging, level_name, logging.INFO)
29
+ logging.basicConfig(format="%(message)s", level=level)
30
+ structlog.configure(
31
+ processors=[
32
+ structlog.contextvars.merge_contextvars,
33
+ structlog.processors.add_log_level,
34
+ structlog.processors.TimeStamper(fmt="iso", utc=True),
35
+ add_otel_context,
36
+ structlog.processors.JSONRenderer(),
37
+ ],
38
+ logger_factory=structlog.stdlib.LoggerFactory(),
39
+ wrapper_class=structlog.make_filtering_bound_logger(level),
40
+ cache_logger_on_first_use=True,
41
+ )
File without changes
File without changes