truthound 1.0.8__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 (877) hide show
  1. truthound/__init__.py +162 -0
  2. truthound/adapters.py +100 -0
  3. truthound/api.py +365 -0
  4. truthound/audit/__init__.py +248 -0
  5. truthound/audit/core.py +967 -0
  6. truthound/audit/filters.py +620 -0
  7. truthound/audit/formatters.py +707 -0
  8. truthound/audit/logger.py +902 -0
  9. truthound/audit/middleware.py +571 -0
  10. truthound/audit/storage.py +1083 -0
  11. truthound/benchmark/__init__.py +123 -0
  12. truthound/benchmark/base.py +757 -0
  13. truthound/benchmark/comparison.py +635 -0
  14. truthound/benchmark/generators.py +706 -0
  15. truthound/benchmark/reporters.py +718 -0
  16. truthound/benchmark/runner.py +635 -0
  17. truthound/benchmark/scenarios.py +712 -0
  18. truthound/cache.py +252 -0
  19. truthound/checkpoint/__init__.py +136 -0
  20. truthound/checkpoint/actions/__init__.py +164 -0
  21. truthound/checkpoint/actions/base.py +324 -0
  22. truthound/checkpoint/actions/custom.py +234 -0
  23. truthound/checkpoint/actions/discord_notify.py +290 -0
  24. truthound/checkpoint/actions/email_notify.py +405 -0
  25. truthound/checkpoint/actions/github_action.py +406 -0
  26. truthound/checkpoint/actions/opsgenie.py +1499 -0
  27. truthound/checkpoint/actions/pagerduty.py +226 -0
  28. truthound/checkpoint/actions/slack_notify.py +233 -0
  29. truthound/checkpoint/actions/store_result.py +249 -0
  30. truthound/checkpoint/actions/teams_notify.py +1570 -0
  31. truthound/checkpoint/actions/telegram_notify.py +419 -0
  32. truthound/checkpoint/actions/update_docs.py +552 -0
  33. truthound/checkpoint/actions/webhook.py +293 -0
  34. truthound/checkpoint/analytics/__init__.py +147 -0
  35. truthound/checkpoint/analytics/aggregations/__init__.py +23 -0
  36. truthound/checkpoint/analytics/aggregations/rollup.py +481 -0
  37. truthound/checkpoint/analytics/aggregations/time_bucket.py +306 -0
  38. truthound/checkpoint/analytics/analyzers/__init__.py +17 -0
  39. truthound/checkpoint/analytics/analyzers/anomaly.py +386 -0
  40. truthound/checkpoint/analytics/analyzers/base.py +270 -0
  41. truthound/checkpoint/analytics/analyzers/forecast.py +421 -0
  42. truthound/checkpoint/analytics/analyzers/trend.py +314 -0
  43. truthound/checkpoint/analytics/models.py +292 -0
  44. truthound/checkpoint/analytics/protocols.py +549 -0
  45. truthound/checkpoint/analytics/service.py +718 -0
  46. truthound/checkpoint/analytics/stores/__init__.py +16 -0
  47. truthound/checkpoint/analytics/stores/base.py +306 -0
  48. truthound/checkpoint/analytics/stores/memory_store.py +353 -0
  49. truthound/checkpoint/analytics/stores/sqlite_store.py +557 -0
  50. truthound/checkpoint/analytics/stores/timescale_store.py +501 -0
  51. truthound/checkpoint/async_actions.py +794 -0
  52. truthound/checkpoint/async_base.py +708 -0
  53. truthound/checkpoint/async_checkpoint.py +617 -0
  54. truthound/checkpoint/async_runner.py +639 -0
  55. truthound/checkpoint/checkpoint.py +527 -0
  56. truthound/checkpoint/ci/__init__.py +61 -0
  57. truthound/checkpoint/ci/detector.py +355 -0
  58. truthound/checkpoint/ci/reporter.py +436 -0
  59. truthound/checkpoint/ci/templates.py +454 -0
  60. truthound/checkpoint/circuitbreaker/__init__.py +133 -0
  61. truthound/checkpoint/circuitbreaker/breaker.py +542 -0
  62. truthound/checkpoint/circuitbreaker/core.py +252 -0
  63. truthound/checkpoint/circuitbreaker/detection.py +459 -0
  64. truthound/checkpoint/circuitbreaker/middleware.py +389 -0
  65. truthound/checkpoint/circuitbreaker/registry.py +357 -0
  66. truthound/checkpoint/distributed/__init__.py +139 -0
  67. truthound/checkpoint/distributed/backends/__init__.py +35 -0
  68. truthound/checkpoint/distributed/backends/celery_backend.py +503 -0
  69. truthound/checkpoint/distributed/backends/kubernetes_backend.py +696 -0
  70. truthound/checkpoint/distributed/backends/local_backend.py +397 -0
  71. truthound/checkpoint/distributed/backends/ray_backend.py +625 -0
  72. truthound/checkpoint/distributed/base.py +774 -0
  73. truthound/checkpoint/distributed/orchestrator.py +765 -0
  74. truthound/checkpoint/distributed/protocols.py +842 -0
  75. truthound/checkpoint/distributed/registry.py +449 -0
  76. truthound/checkpoint/idempotency/__init__.py +120 -0
  77. truthound/checkpoint/idempotency/core.py +295 -0
  78. truthound/checkpoint/idempotency/fingerprint.py +454 -0
  79. truthound/checkpoint/idempotency/locking.py +604 -0
  80. truthound/checkpoint/idempotency/service.py +592 -0
  81. truthound/checkpoint/idempotency/stores.py +653 -0
  82. truthound/checkpoint/monitoring/__init__.py +134 -0
  83. truthound/checkpoint/monitoring/aggregators/__init__.py +15 -0
  84. truthound/checkpoint/monitoring/aggregators/base.py +372 -0
  85. truthound/checkpoint/monitoring/aggregators/realtime.py +300 -0
  86. truthound/checkpoint/monitoring/aggregators/window.py +493 -0
  87. truthound/checkpoint/monitoring/collectors/__init__.py +17 -0
  88. truthound/checkpoint/monitoring/collectors/base.py +257 -0
  89. truthound/checkpoint/monitoring/collectors/memory_collector.py +617 -0
  90. truthound/checkpoint/monitoring/collectors/prometheus_collector.py +451 -0
  91. truthound/checkpoint/monitoring/collectors/redis_collector.py +518 -0
  92. truthound/checkpoint/monitoring/events.py +410 -0
  93. truthound/checkpoint/monitoring/protocols.py +636 -0
  94. truthound/checkpoint/monitoring/service.py +578 -0
  95. truthound/checkpoint/monitoring/views/__init__.py +17 -0
  96. truthound/checkpoint/monitoring/views/base.py +172 -0
  97. truthound/checkpoint/monitoring/views/queue_view.py +220 -0
  98. truthound/checkpoint/monitoring/views/task_view.py +240 -0
  99. truthound/checkpoint/monitoring/views/worker_view.py +263 -0
  100. truthound/checkpoint/registry.py +337 -0
  101. truthound/checkpoint/runner.py +356 -0
  102. truthound/checkpoint/transaction/__init__.py +133 -0
  103. truthound/checkpoint/transaction/base.py +389 -0
  104. truthound/checkpoint/transaction/compensatable.py +537 -0
  105. truthound/checkpoint/transaction/coordinator.py +576 -0
  106. truthound/checkpoint/transaction/executor.py +622 -0
  107. truthound/checkpoint/transaction/idempotency.py +534 -0
  108. truthound/checkpoint/transaction/saga/__init__.py +143 -0
  109. truthound/checkpoint/transaction/saga/builder.py +584 -0
  110. truthound/checkpoint/transaction/saga/definition.py +515 -0
  111. truthound/checkpoint/transaction/saga/event_store.py +542 -0
  112. truthound/checkpoint/transaction/saga/patterns.py +833 -0
  113. truthound/checkpoint/transaction/saga/runner.py +718 -0
  114. truthound/checkpoint/transaction/saga/state_machine.py +793 -0
  115. truthound/checkpoint/transaction/saga/strategies.py +780 -0
  116. truthound/checkpoint/transaction/saga/testing.py +886 -0
  117. truthound/checkpoint/triggers/__init__.py +58 -0
  118. truthound/checkpoint/triggers/base.py +237 -0
  119. truthound/checkpoint/triggers/event.py +385 -0
  120. truthound/checkpoint/triggers/schedule.py +355 -0
  121. truthound/cli.py +2358 -0
  122. truthound/cli_modules/__init__.py +124 -0
  123. truthound/cli_modules/advanced/__init__.py +45 -0
  124. truthound/cli_modules/advanced/benchmark.py +343 -0
  125. truthound/cli_modules/advanced/docs.py +225 -0
  126. truthound/cli_modules/advanced/lineage.py +209 -0
  127. truthound/cli_modules/advanced/ml.py +320 -0
  128. truthound/cli_modules/advanced/realtime.py +196 -0
  129. truthound/cli_modules/checkpoint/__init__.py +46 -0
  130. truthound/cli_modules/checkpoint/init.py +114 -0
  131. truthound/cli_modules/checkpoint/list.py +71 -0
  132. truthound/cli_modules/checkpoint/run.py +159 -0
  133. truthound/cli_modules/checkpoint/validate.py +67 -0
  134. truthound/cli_modules/common/__init__.py +71 -0
  135. truthound/cli_modules/common/errors.py +414 -0
  136. truthound/cli_modules/common/options.py +419 -0
  137. truthound/cli_modules/common/output.py +507 -0
  138. truthound/cli_modules/common/protocol.py +552 -0
  139. truthound/cli_modules/core/__init__.py +48 -0
  140. truthound/cli_modules/core/check.py +123 -0
  141. truthound/cli_modules/core/compare.py +104 -0
  142. truthound/cli_modules/core/learn.py +57 -0
  143. truthound/cli_modules/core/mask.py +77 -0
  144. truthound/cli_modules/core/profile.py +65 -0
  145. truthound/cli_modules/core/scan.py +61 -0
  146. truthound/cli_modules/profiler/__init__.py +51 -0
  147. truthound/cli_modules/profiler/auto_profile.py +175 -0
  148. truthound/cli_modules/profiler/metadata.py +107 -0
  149. truthound/cli_modules/profiler/suite.py +283 -0
  150. truthound/cli_modules/registry.py +431 -0
  151. truthound/cli_modules/scaffolding/__init__.py +89 -0
  152. truthound/cli_modules/scaffolding/base.py +631 -0
  153. truthound/cli_modules/scaffolding/commands.py +545 -0
  154. truthound/cli_modules/scaffolding/plugins.py +1072 -0
  155. truthound/cli_modules/scaffolding/reporters.py +594 -0
  156. truthound/cli_modules/scaffolding/validators.py +1127 -0
  157. truthound/common/__init__.py +18 -0
  158. truthound/common/resilience/__init__.py +130 -0
  159. truthound/common/resilience/bulkhead.py +266 -0
  160. truthound/common/resilience/circuit_breaker.py +516 -0
  161. truthound/common/resilience/composite.py +332 -0
  162. truthound/common/resilience/config.py +292 -0
  163. truthound/common/resilience/protocols.py +217 -0
  164. truthound/common/resilience/rate_limiter.py +404 -0
  165. truthound/common/resilience/retry.py +341 -0
  166. truthound/datadocs/__init__.py +260 -0
  167. truthound/datadocs/base.py +571 -0
  168. truthound/datadocs/builder.py +761 -0
  169. truthound/datadocs/charts.py +764 -0
  170. truthound/datadocs/dashboard/__init__.py +63 -0
  171. truthound/datadocs/dashboard/app.py +576 -0
  172. truthound/datadocs/dashboard/components.py +584 -0
  173. truthound/datadocs/dashboard/state.py +240 -0
  174. truthound/datadocs/engine/__init__.py +46 -0
  175. truthound/datadocs/engine/context.py +376 -0
  176. truthound/datadocs/engine/pipeline.py +618 -0
  177. truthound/datadocs/engine/registry.py +469 -0
  178. truthound/datadocs/exporters/__init__.py +49 -0
  179. truthound/datadocs/exporters/base.py +198 -0
  180. truthound/datadocs/exporters/html.py +178 -0
  181. truthound/datadocs/exporters/json_exporter.py +253 -0
  182. truthound/datadocs/exporters/markdown.py +284 -0
  183. truthound/datadocs/exporters/pdf.py +392 -0
  184. truthound/datadocs/i18n/__init__.py +86 -0
  185. truthound/datadocs/i18n/catalog.py +960 -0
  186. truthound/datadocs/i18n/formatting.py +505 -0
  187. truthound/datadocs/i18n/loader.py +256 -0
  188. truthound/datadocs/i18n/plurals.py +378 -0
  189. truthound/datadocs/renderers/__init__.py +42 -0
  190. truthound/datadocs/renderers/base.py +401 -0
  191. truthound/datadocs/renderers/custom.py +342 -0
  192. truthound/datadocs/renderers/jinja.py +697 -0
  193. truthound/datadocs/sections.py +736 -0
  194. truthound/datadocs/styles.py +931 -0
  195. truthound/datadocs/themes/__init__.py +101 -0
  196. truthound/datadocs/themes/base.py +336 -0
  197. truthound/datadocs/themes/default.py +417 -0
  198. truthound/datadocs/themes/enterprise.py +419 -0
  199. truthound/datadocs/themes/loader.py +336 -0
  200. truthound/datadocs/themes.py +301 -0
  201. truthound/datadocs/transformers/__init__.py +57 -0
  202. truthound/datadocs/transformers/base.py +268 -0
  203. truthound/datadocs/transformers/enrichers.py +544 -0
  204. truthound/datadocs/transformers/filters.py +447 -0
  205. truthound/datadocs/transformers/i18n.py +468 -0
  206. truthound/datadocs/versioning/__init__.py +62 -0
  207. truthound/datadocs/versioning/diff.py +639 -0
  208. truthound/datadocs/versioning/storage.py +497 -0
  209. truthound/datadocs/versioning/version.py +358 -0
  210. truthound/datasources/__init__.py +223 -0
  211. truthound/datasources/_async_protocols.py +222 -0
  212. truthound/datasources/_protocols.py +159 -0
  213. truthound/datasources/adapters.py +428 -0
  214. truthound/datasources/async_base.py +599 -0
  215. truthound/datasources/async_factory.py +511 -0
  216. truthound/datasources/base.py +516 -0
  217. truthound/datasources/factory.py +433 -0
  218. truthound/datasources/nosql/__init__.py +47 -0
  219. truthound/datasources/nosql/base.py +487 -0
  220. truthound/datasources/nosql/elasticsearch.py +801 -0
  221. truthound/datasources/nosql/mongodb.py +636 -0
  222. truthound/datasources/pandas_optimized.py +582 -0
  223. truthound/datasources/pandas_source.py +216 -0
  224. truthound/datasources/polars_source.py +395 -0
  225. truthound/datasources/spark_source.py +479 -0
  226. truthound/datasources/sql/__init__.py +154 -0
  227. truthound/datasources/sql/base.py +710 -0
  228. truthound/datasources/sql/bigquery.py +410 -0
  229. truthound/datasources/sql/cloud_base.py +199 -0
  230. truthound/datasources/sql/databricks.py +471 -0
  231. truthound/datasources/sql/mysql.py +316 -0
  232. truthound/datasources/sql/oracle.py +427 -0
  233. truthound/datasources/sql/postgresql.py +321 -0
  234. truthound/datasources/sql/redshift.py +479 -0
  235. truthound/datasources/sql/snowflake.py +439 -0
  236. truthound/datasources/sql/sqlite.py +286 -0
  237. truthound/datasources/sql/sqlserver.py +437 -0
  238. truthound/datasources/streaming/__init__.py +47 -0
  239. truthound/datasources/streaming/base.py +350 -0
  240. truthound/datasources/streaming/kafka.py +670 -0
  241. truthound/decorators.py +98 -0
  242. truthound/docs/__init__.py +69 -0
  243. truthound/docs/extractor.py +971 -0
  244. truthound/docs/generator.py +601 -0
  245. truthound/docs/parser.py +1037 -0
  246. truthound/docs/renderer.py +999 -0
  247. truthound/drift/__init__.py +22 -0
  248. truthound/drift/compare.py +189 -0
  249. truthound/drift/detectors.py +464 -0
  250. truthound/drift/report.py +160 -0
  251. truthound/execution/__init__.py +65 -0
  252. truthound/execution/_protocols.py +324 -0
  253. truthound/execution/base.py +576 -0
  254. truthound/execution/distributed/__init__.py +179 -0
  255. truthound/execution/distributed/aggregations.py +731 -0
  256. truthound/execution/distributed/arrow_bridge.py +817 -0
  257. truthound/execution/distributed/base.py +550 -0
  258. truthound/execution/distributed/dask_engine.py +976 -0
  259. truthound/execution/distributed/mixins.py +766 -0
  260. truthound/execution/distributed/protocols.py +756 -0
  261. truthound/execution/distributed/ray_engine.py +1127 -0
  262. truthound/execution/distributed/registry.py +446 -0
  263. truthound/execution/distributed/spark_engine.py +1011 -0
  264. truthound/execution/distributed/validator_adapter.py +682 -0
  265. truthound/execution/pandas_engine.py +401 -0
  266. truthound/execution/polars_engine.py +497 -0
  267. truthound/execution/pushdown/__init__.py +230 -0
  268. truthound/execution/pushdown/ast.py +1550 -0
  269. truthound/execution/pushdown/builder.py +1550 -0
  270. truthound/execution/pushdown/dialects.py +1072 -0
  271. truthound/execution/pushdown/executor.py +829 -0
  272. truthound/execution/pushdown/optimizer.py +1041 -0
  273. truthound/execution/sql_engine.py +518 -0
  274. truthound/infrastructure/__init__.py +189 -0
  275. truthound/infrastructure/audit.py +1515 -0
  276. truthound/infrastructure/config.py +1133 -0
  277. truthound/infrastructure/encryption.py +1132 -0
  278. truthound/infrastructure/logging.py +1503 -0
  279. truthound/infrastructure/metrics.py +1220 -0
  280. truthound/lineage/__init__.py +89 -0
  281. truthound/lineage/base.py +746 -0
  282. truthound/lineage/impact_analysis.py +474 -0
  283. truthound/lineage/integrations/__init__.py +22 -0
  284. truthound/lineage/integrations/openlineage.py +548 -0
  285. truthound/lineage/tracker.py +512 -0
  286. truthound/lineage/visualization/__init__.py +33 -0
  287. truthound/lineage/visualization/protocols.py +145 -0
  288. truthound/lineage/visualization/renderers/__init__.py +20 -0
  289. truthound/lineage/visualization/renderers/cytoscape.py +329 -0
  290. truthound/lineage/visualization/renderers/d3.py +331 -0
  291. truthound/lineage/visualization/renderers/graphviz.py +276 -0
  292. truthound/lineage/visualization/renderers/mermaid.py +308 -0
  293. truthound/maskers.py +113 -0
  294. truthound/ml/__init__.py +124 -0
  295. truthound/ml/anomaly_models/__init__.py +31 -0
  296. truthound/ml/anomaly_models/ensemble.py +362 -0
  297. truthound/ml/anomaly_models/isolation_forest.py +444 -0
  298. truthound/ml/anomaly_models/statistical.py +392 -0
  299. truthound/ml/base.py +1178 -0
  300. truthound/ml/drift_detection/__init__.py +26 -0
  301. truthound/ml/drift_detection/concept.py +381 -0
  302. truthound/ml/drift_detection/distribution.py +361 -0
  303. truthound/ml/drift_detection/feature.py +442 -0
  304. truthound/ml/drift_detection/multivariate.py +495 -0
  305. truthound/ml/monitoring/__init__.py +88 -0
  306. truthound/ml/monitoring/alerting/__init__.py +33 -0
  307. truthound/ml/monitoring/alerting/handlers.py +427 -0
  308. truthound/ml/monitoring/alerting/rules.py +508 -0
  309. truthound/ml/monitoring/collectors/__init__.py +19 -0
  310. truthound/ml/monitoring/collectors/composite.py +105 -0
  311. truthound/ml/monitoring/collectors/drift.py +324 -0
  312. truthound/ml/monitoring/collectors/performance.py +179 -0
  313. truthound/ml/monitoring/collectors/quality.py +369 -0
  314. truthound/ml/monitoring/monitor.py +536 -0
  315. truthound/ml/monitoring/protocols.py +451 -0
  316. truthound/ml/monitoring/stores/__init__.py +15 -0
  317. truthound/ml/monitoring/stores/memory.py +201 -0
  318. truthound/ml/monitoring/stores/prometheus.py +296 -0
  319. truthound/ml/rule_learning/__init__.py +25 -0
  320. truthound/ml/rule_learning/constraint_miner.py +443 -0
  321. truthound/ml/rule_learning/pattern_learner.py +499 -0
  322. truthound/ml/rule_learning/profile_learner.py +462 -0
  323. truthound/multitenancy/__init__.py +326 -0
  324. truthound/multitenancy/core.py +852 -0
  325. truthound/multitenancy/integration.py +597 -0
  326. truthound/multitenancy/isolation.py +630 -0
  327. truthound/multitenancy/manager.py +770 -0
  328. truthound/multitenancy/middleware.py +765 -0
  329. truthound/multitenancy/quota.py +537 -0
  330. truthound/multitenancy/resolvers.py +603 -0
  331. truthound/multitenancy/storage.py +703 -0
  332. truthound/observability/__init__.py +307 -0
  333. truthound/observability/context.py +531 -0
  334. truthound/observability/instrumentation.py +611 -0
  335. truthound/observability/logging.py +887 -0
  336. truthound/observability/metrics.py +1157 -0
  337. truthound/observability/tracing/__init__.py +178 -0
  338. truthound/observability/tracing/baggage.py +310 -0
  339. truthound/observability/tracing/config.py +426 -0
  340. truthound/observability/tracing/exporter.py +787 -0
  341. truthound/observability/tracing/integration.py +1018 -0
  342. truthound/observability/tracing/otel/__init__.py +146 -0
  343. truthound/observability/tracing/otel/adapter.py +982 -0
  344. truthound/observability/tracing/otel/bridge.py +1177 -0
  345. truthound/observability/tracing/otel/compat.py +681 -0
  346. truthound/observability/tracing/otel/config.py +691 -0
  347. truthound/observability/tracing/otel/detection.py +327 -0
  348. truthound/observability/tracing/otel/protocols.py +426 -0
  349. truthound/observability/tracing/processor.py +561 -0
  350. truthound/observability/tracing/propagator.py +757 -0
  351. truthound/observability/tracing/provider.py +569 -0
  352. truthound/observability/tracing/resource.py +515 -0
  353. truthound/observability/tracing/sampler.py +487 -0
  354. truthound/observability/tracing/span.py +676 -0
  355. truthound/plugins/__init__.py +198 -0
  356. truthound/plugins/base.py +599 -0
  357. truthound/plugins/cli.py +680 -0
  358. truthound/plugins/dependencies/__init__.py +42 -0
  359. truthound/plugins/dependencies/graph.py +422 -0
  360. truthound/plugins/dependencies/resolver.py +417 -0
  361. truthound/plugins/discovery.py +379 -0
  362. truthound/plugins/docs/__init__.py +46 -0
  363. truthound/plugins/docs/extractor.py +444 -0
  364. truthound/plugins/docs/renderer.py +499 -0
  365. truthound/plugins/enterprise_manager.py +877 -0
  366. truthound/plugins/examples/__init__.py +19 -0
  367. truthound/plugins/examples/custom_validators.py +317 -0
  368. truthound/plugins/examples/slack_notifier.py +312 -0
  369. truthound/plugins/examples/xml_reporter.py +254 -0
  370. truthound/plugins/hooks.py +558 -0
  371. truthound/plugins/lifecycle/__init__.py +43 -0
  372. truthound/plugins/lifecycle/hot_reload.py +402 -0
  373. truthound/plugins/lifecycle/manager.py +371 -0
  374. truthound/plugins/manager.py +736 -0
  375. truthound/plugins/registry.py +338 -0
  376. truthound/plugins/security/__init__.py +93 -0
  377. truthound/plugins/security/exceptions.py +332 -0
  378. truthound/plugins/security/policies.py +348 -0
  379. truthound/plugins/security/protocols.py +643 -0
  380. truthound/plugins/security/sandbox/__init__.py +45 -0
  381. truthound/plugins/security/sandbox/context.py +158 -0
  382. truthound/plugins/security/sandbox/engines/__init__.py +19 -0
  383. truthound/plugins/security/sandbox/engines/container.py +379 -0
  384. truthound/plugins/security/sandbox/engines/noop.py +144 -0
  385. truthound/plugins/security/sandbox/engines/process.py +336 -0
  386. truthound/plugins/security/sandbox/factory.py +211 -0
  387. truthound/plugins/security/signing/__init__.py +57 -0
  388. truthound/plugins/security/signing/service.py +330 -0
  389. truthound/plugins/security/signing/trust_store.py +368 -0
  390. truthound/plugins/security/signing/verifier.py +459 -0
  391. truthound/plugins/versioning/__init__.py +41 -0
  392. truthound/plugins/versioning/constraints.py +297 -0
  393. truthound/plugins/versioning/resolver.py +329 -0
  394. truthound/profiler/__init__.py +1729 -0
  395. truthound/profiler/_lazy.py +452 -0
  396. truthound/profiler/ab_testing/__init__.py +80 -0
  397. truthound/profiler/ab_testing/analysis.py +449 -0
  398. truthound/profiler/ab_testing/base.py +257 -0
  399. truthound/profiler/ab_testing/experiment.py +395 -0
  400. truthound/profiler/ab_testing/tracking.py +368 -0
  401. truthound/profiler/auto_threshold.py +1170 -0
  402. truthound/profiler/base.py +579 -0
  403. truthound/profiler/cache_patterns.py +911 -0
  404. truthound/profiler/caching.py +1303 -0
  405. truthound/profiler/column_profiler.py +712 -0
  406. truthound/profiler/comparison.py +1007 -0
  407. truthound/profiler/custom_patterns.py +1170 -0
  408. truthound/profiler/dashboard/__init__.py +50 -0
  409. truthound/profiler/dashboard/app.py +476 -0
  410. truthound/profiler/dashboard/components.py +457 -0
  411. truthound/profiler/dashboard/config.py +72 -0
  412. truthound/profiler/distributed/__init__.py +83 -0
  413. truthound/profiler/distributed/base.py +281 -0
  414. truthound/profiler/distributed/dask_backend.py +498 -0
  415. truthound/profiler/distributed/local_backend.py +293 -0
  416. truthound/profiler/distributed/profiler.py +304 -0
  417. truthound/profiler/distributed/ray_backend.py +374 -0
  418. truthound/profiler/distributed/spark_backend.py +375 -0
  419. truthound/profiler/distributed.py +1366 -0
  420. truthound/profiler/enterprise_sampling.py +1065 -0
  421. truthound/profiler/errors.py +488 -0
  422. truthound/profiler/evolution/__init__.py +91 -0
  423. truthound/profiler/evolution/alerts.py +426 -0
  424. truthound/profiler/evolution/changes.py +206 -0
  425. truthound/profiler/evolution/compatibility.py +365 -0
  426. truthound/profiler/evolution/detector.py +372 -0
  427. truthound/profiler/evolution/protocols.py +121 -0
  428. truthound/profiler/generators/__init__.py +48 -0
  429. truthound/profiler/generators/base.py +384 -0
  430. truthound/profiler/generators/ml_rules.py +375 -0
  431. truthound/profiler/generators/pattern_rules.py +384 -0
  432. truthound/profiler/generators/schema_rules.py +267 -0
  433. truthound/profiler/generators/stats_rules.py +324 -0
  434. truthound/profiler/generators/suite_generator.py +857 -0
  435. truthound/profiler/i18n.py +1542 -0
  436. truthound/profiler/incremental.py +554 -0
  437. truthound/profiler/incremental_validation.py +1710 -0
  438. truthound/profiler/integration/__init__.py +73 -0
  439. truthound/profiler/integration/adapters.py +345 -0
  440. truthound/profiler/integration/context.py +371 -0
  441. truthound/profiler/integration/executor.py +527 -0
  442. truthound/profiler/integration/naming.py +75 -0
  443. truthound/profiler/integration/protocols.py +243 -0
  444. truthound/profiler/memory.py +1185 -0
  445. truthound/profiler/migration/__init__.py +60 -0
  446. truthound/profiler/migration/base.py +345 -0
  447. truthound/profiler/migration/manager.py +444 -0
  448. truthound/profiler/migration/v1_0_to_v1_1.py +484 -0
  449. truthound/profiler/ml/__init__.py +73 -0
  450. truthound/profiler/ml/base.py +244 -0
  451. truthound/profiler/ml/classifier.py +507 -0
  452. truthound/profiler/ml/feature_extraction.py +604 -0
  453. truthound/profiler/ml/pretrained.py +448 -0
  454. truthound/profiler/ml_inference.py +1276 -0
  455. truthound/profiler/native_patterns.py +815 -0
  456. truthound/profiler/observability.py +1184 -0
  457. truthound/profiler/process_timeout.py +1566 -0
  458. truthound/profiler/progress.py +568 -0
  459. truthound/profiler/progress_callbacks.py +1734 -0
  460. truthound/profiler/quality.py +1345 -0
  461. truthound/profiler/resilience.py +1180 -0
  462. truthound/profiler/sampled_matcher.py +794 -0
  463. truthound/profiler/sampling.py +1288 -0
  464. truthound/profiler/scheduling/__init__.py +82 -0
  465. truthound/profiler/scheduling/protocols.py +214 -0
  466. truthound/profiler/scheduling/scheduler.py +474 -0
  467. truthound/profiler/scheduling/storage.py +457 -0
  468. truthound/profiler/scheduling/triggers.py +449 -0
  469. truthound/profiler/schema.py +603 -0
  470. truthound/profiler/streaming.py +685 -0
  471. truthound/profiler/streaming_patterns.py +1354 -0
  472. truthound/profiler/suite_cli.py +625 -0
  473. truthound/profiler/suite_config.py +789 -0
  474. truthound/profiler/suite_export.py +1268 -0
  475. truthound/profiler/table_profiler.py +547 -0
  476. truthound/profiler/timeout.py +565 -0
  477. truthound/profiler/validation.py +1532 -0
  478. truthound/profiler/visualization/__init__.py +118 -0
  479. truthound/profiler/visualization/base.py +346 -0
  480. truthound/profiler/visualization/generator.py +1259 -0
  481. truthound/profiler/visualization/plotly_renderer.py +811 -0
  482. truthound/profiler/visualization/renderers.py +669 -0
  483. truthound/profiler/visualization/sections.py +540 -0
  484. truthound/profiler/visualization.py +2122 -0
  485. truthound/profiler/yaml_validation.py +1151 -0
  486. truthound/py.typed +0 -0
  487. truthound/ratelimit/__init__.py +248 -0
  488. truthound/ratelimit/algorithms.py +1108 -0
  489. truthound/ratelimit/core.py +573 -0
  490. truthound/ratelimit/integration.py +532 -0
  491. truthound/ratelimit/limiter.py +663 -0
  492. truthound/ratelimit/middleware.py +700 -0
  493. truthound/ratelimit/policy.py +792 -0
  494. truthound/ratelimit/storage.py +763 -0
  495. truthound/rbac/__init__.py +340 -0
  496. truthound/rbac/core.py +976 -0
  497. truthound/rbac/integration.py +760 -0
  498. truthound/rbac/manager.py +1052 -0
  499. truthound/rbac/middleware.py +842 -0
  500. truthound/rbac/policy.py +954 -0
  501. truthound/rbac/storage.py +878 -0
  502. truthound/realtime/__init__.py +141 -0
  503. truthound/realtime/adapters/__init__.py +43 -0
  504. truthound/realtime/adapters/base.py +533 -0
  505. truthound/realtime/adapters/kafka.py +487 -0
  506. truthound/realtime/adapters/kinesis.py +479 -0
  507. truthound/realtime/adapters/mock.py +243 -0
  508. truthound/realtime/base.py +553 -0
  509. truthound/realtime/factory.py +382 -0
  510. truthound/realtime/incremental.py +660 -0
  511. truthound/realtime/processing/__init__.py +67 -0
  512. truthound/realtime/processing/exactly_once.py +575 -0
  513. truthound/realtime/processing/state.py +547 -0
  514. truthound/realtime/processing/windows.py +647 -0
  515. truthound/realtime/protocols.py +569 -0
  516. truthound/realtime/streaming.py +605 -0
  517. truthound/realtime/testing/__init__.py +32 -0
  518. truthound/realtime/testing/containers.py +615 -0
  519. truthound/realtime/testing/fixtures.py +484 -0
  520. truthound/report.py +280 -0
  521. truthound/reporters/__init__.py +46 -0
  522. truthound/reporters/_protocols.py +30 -0
  523. truthound/reporters/base.py +324 -0
  524. truthound/reporters/ci/__init__.py +66 -0
  525. truthound/reporters/ci/azure.py +436 -0
  526. truthound/reporters/ci/base.py +509 -0
  527. truthound/reporters/ci/bitbucket.py +567 -0
  528. truthound/reporters/ci/circleci.py +547 -0
  529. truthound/reporters/ci/detection.py +364 -0
  530. truthound/reporters/ci/factory.py +182 -0
  531. truthound/reporters/ci/github.py +388 -0
  532. truthound/reporters/ci/gitlab.py +471 -0
  533. truthound/reporters/ci/jenkins.py +525 -0
  534. truthound/reporters/console_reporter.py +299 -0
  535. truthound/reporters/factory.py +211 -0
  536. truthound/reporters/html_reporter.py +524 -0
  537. truthound/reporters/json_reporter.py +256 -0
  538. truthound/reporters/markdown_reporter.py +280 -0
  539. truthound/reporters/sdk/__init__.py +174 -0
  540. truthound/reporters/sdk/builder.py +558 -0
  541. truthound/reporters/sdk/mixins.py +1150 -0
  542. truthound/reporters/sdk/schema.py +1493 -0
  543. truthound/reporters/sdk/templates.py +666 -0
  544. truthound/reporters/sdk/testing.py +968 -0
  545. truthound/scanners.py +170 -0
  546. truthound/scheduling/__init__.py +122 -0
  547. truthound/scheduling/cron.py +1136 -0
  548. truthound/scheduling/presets.py +212 -0
  549. truthound/schema.py +275 -0
  550. truthound/secrets/__init__.py +173 -0
  551. truthound/secrets/base.py +618 -0
  552. truthound/secrets/cloud.py +682 -0
  553. truthound/secrets/integration.py +507 -0
  554. truthound/secrets/manager.py +633 -0
  555. truthound/secrets/oidc/__init__.py +172 -0
  556. truthound/secrets/oidc/base.py +902 -0
  557. truthound/secrets/oidc/credential_provider.py +623 -0
  558. truthound/secrets/oidc/exchangers.py +1001 -0
  559. truthound/secrets/oidc/github/__init__.py +110 -0
  560. truthound/secrets/oidc/github/claims.py +718 -0
  561. truthound/secrets/oidc/github/enhanced_provider.py +693 -0
  562. truthound/secrets/oidc/github/trust_policy.py +742 -0
  563. truthound/secrets/oidc/github/verification.py +723 -0
  564. truthound/secrets/oidc/github/workflow.py +691 -0
  565. truthound/secrets/oidc/providers.py +825 -0
  566. truthound/secrets/providers.py +506 -0
  567. truthound/secrets/resolver.py +495 -0
  568. truthound/stores/__init__.py +177 -0
  569. truthound/stores/backends/__init__.py +18 -0
  570. truthound/stores/backends/_protocols.py +340 -0
  571. truthound/stores/backends/azure_blob.py +530 -0
  572. truthound/stores/backends/concurrent_filesystem.py +915 -0
  573. truthound/stores/backends/connection_pool.py +1365 -0
  574. truthound/stores/backends/database.py +743 -0
  575. truthound/stores/backends/filesystem.py +538 -0
  576. truthound/stores/backends/gcs.py +399 -0
  577. truthound/stores/backends/memory.py +354 -0
  578. truthound/stores/backends/s3.py +434 -0
  579. truthound/stores/backpressure/__init__.py +84 -0
  580. truthound/stores/backpressure/base.py +375 -0
  581. truthound/stores/backpressure/circuit_breaker.py +434 -0
  582. truthound/stores/backpressure/monitor.py +376 -0
  583. truthound/stores/backpressure/strategies.py +677 -0
  584. truthound/stores/base.py +551 -0
  585. truthound/stores/batching/__init__.py +65 -0
  586. truthound/stores/batching/base.py +305 -0
  587. truthound/stores/batching/buffer.py +370 -0
  588. truthound/stores/batching/store.py +248 -0
  589. truthound/stores/batching/writer.py +521 -0
  590. truthound/stores/caching/__init__.py +60 -0
  591. truthound/stores/caching/backends.py +684 -0
  592. truthound/stores/caching/base.py +356 -0
  593. truthound/stores/caching/store.py +305 -0
  594. truthound/stores/compression/__init__.py +193 -0
  595. truthound/stores/compression/adaptive.py +694 -0
  596. truthound/stores/compression/base.py +514 -0
  597. truthound/stores/compression/pipeline.py +868 -0
  598. truthound/stores/compression/providers.py +672 -0
  599. truthound/stores/compression/streaming.py +832 -0
  600. truthound/stores/concurrency/__init__.py +81 -0
  601. truthound/stores/concurrency/atomic.py +556 -0
  602. truthound/stores/concurrency/index.py +775 -0
  603. truthound/stores/concurrency/locks.py +576 -0
  604. truthound/stores/concurrency/manager.py +482 -0
  605. truthound/stores/encryption/__init__.py +297 -0
  606. truthound/stores/encryption/base.py +952 -0
  607. truthound/stores/encryption/keys.py +1191 -0
  608. truthound/stores/encryption/pipeline.py +903 -0
  609. truthound/stores/encryption/providers.py +953 -0
  610. truthound/stores/encryption/streaming.py +950 -0
  611. truthound/stores/expectations.py +227 -0
  612. truthound/stores/factory.py +246 -0
  613. truthound/stores/migration/__init__.py +75 -0
  614. truthound/stores/migration/base.py +480 -0
  615. truthound/stores/migration/manager.py +347 -0
  616. truthound/stores/migration/registry.py +382 -0
  617. truthound/stores/migration/store.py +559 -0
  618. truthound/stores/observability/__init__.py +106 -0
  619. truthound/stores/observability/audit.py +718 -0
  620. truthound/stores/observability/config.py +270 -0
  621. truthound/stores/observability/factory.py +208 -0
  622. truthound/stores/observability/metrics.py +636 -0
  623. truthound/stores/observability/protocols.py +410 -0
  624. truthound/stores/observability/store.py +570 -0
  625. truthound/stores/observability/tracing.py +784 -0
  626. truthound/stores/replication/__init__.py +76 -0
  627. truthound/stores/replication/base.py +260 -0
  628. truthound/stores/replication/monitor.py +269 -0
  629. truthound/stores/replication/store.py +439 -0
  630. truthound/stores/replication/syncer.py +391 -0
  631. truthound/stores/results.py +359 -0
  632. truthound/stores/retention/__init__.py +77 -0
  633. truthound/stores/retention/base.py +378 -0
  634. truthound/stores/retention/policies.py +621 -0
  635. truthound/stores/retention/scheduler.py +279 -0
  636. truthound/stores/retention/store.py +526 -0
  637. truthound/stores/streaming/__init__.py +138 -0
  638. truthound/stores/streaming/base.py +801 -0
  639. truthound/stores/streaming/database.py +984 -0
  640. truthound/stores/streaming/filesystem.py +719 -0
  641. truthound/stores/streaming/reader.py +629 -0
  642. truthound/stores/streaming/s3.py +843 -0
  643. truthound/stores/streaming/writer.py +790 -0
  644. truthound/stores/tiering/__init__.py +108 -0
  645. truthound/stores/tiering/base.py +462 -0
  646. truthound/stores/tiering/manager.py +249 -0
  647. truthound/stores/tiering/policies.py +692 -0
  648. truthound/stores/tiering/store.py +526 -0
  649. truthound/stores/versioning/__init__.py +56 -0
  650. truthound/stores/versioning/base.py +376 -0
  651. truthound/stores/versioning/store.py +660 -0
  652. truthound/stores/versioning/strategies.py +353 -0
  653. truthound/types.py +56 -0
  654. truthound/validators/__init__.py +774 -0
  655. truthound/validators/aggregate/__init__.py +27 -0
  656. truthound/validators/aggregate/central.py +116 -0
  657. truthound/validators/aggregate/extremes.py +116 -0
  658. truthound/validators/aggregate/spread.py +118 -0
  659. truthound/validators/aggregate/sum.py +64 -0
  660. truthound/validators/aggregate/type.py +78 -0
  661. truthound/validators/anomaly/__init__.py +93 -0
  662. truthound/validators/anomaly/base.py +431 -0
  663. truthound/validators/anomaly/ml_based.py +1190 -0
  664. truthound/validators/anomaly/multivariate.py +647 -0
  665. truthound/validators/anomaly/statistical.py +599 -0
  666. truthound/validators/base.py +1089 -0
  667. truthound/validators/business_rule/__init__.py +46 -0
  668. truthound/validators/business_rule/base.py +147 -0
  669. truthound/validators/business_rule/checksum.py +509 -0
  670. truthound/validators/business_rule/financial.py +526 -0
  671. truthound/validators/cache.py +733 -0
  672. truthound/validators/completeness/__init__.py +39 -0
  673. truthound/validators/completeness/conditional.py +73 -0
  674. truthound/validators/completeness/default.py +98 -0
  675. truthound/validators/completeness/empty.py +103 -0
  676. truthound/validators/completeness/nan.py +337 -0
  677. truthound/validators/completeness/null.py +152 -0
  678. truthound/validators/cross_table/__init__.py +17 -0
  679. truthound/validators/cross_table/aggregate.py +333 -0
  680. truthound/validators/cross_table/row_count.py +122 -0
  681. truthound/validators/datetime/__init__.py +29 -0
  682. truthound/validators/datetime/format.py +78 -0
  683. truthound/validators/datetime/freshness.py +269 -0
  684. truthound/validators/datetime/order.py +73 -0
  685. truthound/validators/datetime/parseable.py +185 -0
  686. truthound/validators/datetime/range.py +202 -0
  687. truthound/validators/datetime/timezone.py +69 -0
  688. truthound/validators/distribution/__init__.py +49 -0
  689. truthound/validators/distribution/distribution.py +128 -0
  690. truthound/validators/distribution/monotonic.py +119 -0
  691. truthound/validators/distribution/outlier.py +178 -0
  692. truthound/validators/distribution/quantile.py +80 -0
  693. truthound/validators/distribution/range.py +254 -0
  694. truthound/validators/distribution/set.py +125 -0
  695. truthound/validators/distribution/statistical.py +459 -0
  696. truthound/validators/drift/__init__.py +79 -0
  697. truthound/validators/drift/base.py +427 -0
  698. truthound/validators/drift/multi_feature.py +401 -0
  699. truthound/validators/drift/numeric.py +395 -0
  700. truthound/validators/drift/psi.py +446 -0
  701. truthound/validators/drift/statistical.py +510 -0
  702. truthound/validators/enterprise.py +1658 -0
  703. truthound/validators/geospatial/__init__.py +80 -0
  704. truthound/validators/geospatial/base.py +97 -0
  705. truthound/validators/geospatial/boundary.py +238 -0
  706. truthound/validators/geospatial/coordinate.py +351 -0
  707. truthound/validators/geospatial/distance.py +399 -0
  708. truthound/validators/geospatial/polygon.py +665 -0
  709. truthound/validators/i18n/__init__.py +308 -0
  710. truthound/validators/i18n/bidi.py +571 -0
  711. truthound/validators/i18n/catalogs.py +570 -0
  712. truthound/validators/i18n/dialects.py +763 -0
  713. truthound/validators/i18n/extended_catalogs.py +549 -0
  714. truthound/validators/i18n/formatting.py +1434 -0
  715. truthound/validators/i18n/loader.py +1020 -0
  716. truthound/validators/i18n/messages.py +521 -0
  717. truthound/validators/i18n/plural.py +683 -0
  718. truthound/validators/i18n/protocols.py +855 -0
  719. truthound/validators/i18n/tms.py +1162 -0
  720. truthound/validators/localization/__init__.py +53 -0
  721. truthound/validators/localization/base.py +122 -0
  722. truthound/validators/localization/chinese.py +362 -0
  723. truthound/validators/localization/japanese.py +275 -0
  724. truthound/validators/localization/korean.py +524 -0
  725. truthound/validators/memory/__init__.py +94 -0
  726. truthound/validators/memory/approximate_knn.py +506 -0
  727. truthound/validators/memory/base.py +547 -0
  728. truthound/validators/memory/sgd_online.py +719 -0
  729. truthound/validators/memory/streaming_ecdf.py +753 -0
  730. truthound/validators/ml_feature/__init__.py +54 -0
  731. truthound/validators/ml_feature/base.py +249 -0
  732. truthound/validators/ml_feature/correlation.py +299 -0
  733. truthound/validators/ml_feature/leakage.py +344 -0
  734. truthound/validators/ml_feature/null_impact.py +270 -0
  735. truthound/validators/ml_feature/scale.py +264 -0
  736. truthound/validators/multi_column/__init__.py +89 -0
  737. truthound/validators/multi_column/arithmetic.py +284 -0
  738. truthound/validators/multi_column/base.py +231 -0
  739. truthound/validators/multi_column/comparison.py +273 -0
  740. truthound/validators/multi_column/consistency.py +312 -0
  741. truthound/validators/multi_column/statistical.py +299 -0
  742. truthound/validators/optimization/__init__.py +164 -0
  743. truthound/validators/optimization/aggregation.py +563 -0
  744. truthound/validators/optimization/covariance.py +556 -0
  745. truthound/validators/optimization/geo.py +626 -0
  746. truthound/validators/optimization/graph.py +587 -0
  747. truthound/validators/optimization/orchestrator.py +970 -0
  748. truthound/validators/optimization/profiling.py +1312 -0
  749. truthound/validators/privacy/__init__.py +223 -0
  750. truthound/validators/privacy/base.py +635 -0
  751. truthound/validators/privacy/ccpa.py +670 -0
  752. truthound/validators/privacy/gdpr.py +728 -0
  753. truthound/validators/privacy/global_patterns.py +604 -0
  754. truthound/validators/privacy/plugins.py +867 -0
  755. truthound/validators/profiling/__init__.py +52 -0
  756. truthound/validators/profiling/base.py +175 -0
  757. truthound/validators/profiling/cardinality.py +312 -0
  758. truthound/validators/profiling/entropy.py +391 -0
  759. truthound/validators/profiling/frequency.py +455 -0
  760. truthound/validators/pushdown_support.py +660 -0
  761. truthound/validators/query/__init__.py +91 -0
  762. truthound/validators/query/aggregate.py +346 -0
  763. truthound/validators/query/base.py +246 -0
  764. truthound/validators/query/column.py +249 -0
  765. truthound/validators/query/expression.py +274 -0
  766. truthound/validators/query/result.py +323 -0
  767. truthound/validators/query/row_count.py +264 -0
  768. truthound/validators/referential/__init__.py +80 -0
  769. truthound/validators/referential/base.py +395 -0
  770. truthound/validators/referential/cascade.py +391 -0
  771. truthound/validators/referential/circular.py +563 -0
  772. truthound/validators/referential/foreign_key.py +624 -0
  773. truthound/validators/referential/orphan.py +485 -0
  774. truthound/validators/registry.py +112 -0
  775. truthound/validators/schema/__init__.py +41 -0
  776. truthound/validators/schema/column_count.py +142 -0
  777. truthound/validators/schema/column_exists.py +80 -0
  778. truthound/validators/schema/column_order.py +82 -0
  779. truthound/validators/schema/column_pair.py +85 -0
  780. truthound/validators/schema/column_pair_set.py +195 -0
  781. truthound/validators/schema/column_type.py +94 -0
  782. truthound/validators/schema/multi_column.py +53 -0
  783. truthound/validators/schema/multi_column_aggregate.py +175 -0
  784. truthound/validators/schema/referential.py +274 -0
  785. truthound/validators/schema/table_schema.py +91 -0
  786. truthound/validators/schema_validator.py +219 -0
  787. truthound/validators/sdk/__init__.py +250 -0
  788. truthound/validators/sdk/builder.py +680 -0
  789. truthound/validators/sdk/decorators.py +474 -0
  790. truthound/validators/sdk/enterprise/__init__.py +211 -0
  791. truthound/validators/sdk/enterprise/docs.py +725 -0
  792. truthound/validators/sdk/enterprise/fuzzing.py +659 -0
  793. truthound/validators/sdk/enterprise/licensing.py +709 -0
  794. truthound/validators/sdk/enterprise/manager.py +543 -0
  795. truthound/validators/sdk/enterprise/resources.py +628 -0
  796. truthound/validators/sdk/enterprise/sandbox.py +766 -0
  797. truthound/validators/sdk/enterprise/signing.py +603 -0
  798. truthound/validators/sdk/enterprise/templates.py +865 -0
  799. truthound/validators/sdk/enterprise/versioning.py +659 -0
  800. truthound/validators/sdk/templates.py +757 -0
  801. truthound/validators/sdk/testing.py +807 -0
  802. truthound/validators/security/__init__.py +181 -0
  803. truthound/validators/security/redos/__init__.py +182 -0
  804. truthound/validators/security/redos/core.py +861 -0
  805. truthound/validators/security/redos/cpu_monitor.py +593 -0
  806. truthound/validators/security/redos/cve_database.py +791 -0
  807. truthound/validators/security/redos/ml/__init__.py +155 -0
  808. truthound/validators/security/redos/ml/base.py +785 -0
  809. truthound/validators/security/redos/ml/datasets.py +618 -0
  810. truthound/validators/security/redos/ml/features.py +359 -0
  811. truthound/validators/security/redos/ml/models.py +1000 -0
  812. truthound/validators/security/redos/ml/predictor.py +507 -0
  813. truthound/validators/security/redos/ml/storage.py +632 -0
  814. truthound/validators/security/redos/ml/training.py +571 -0
  815. truthound/validators/security/redos/ml_analyzer.py +937 -0
  816. truthound/validators/security/redos/optimizer.py +674 -0
  817. truthound/validators/security/redos/profiler.py +682 -0
  818. truthound/validators/security/redos/re2_engine.py +709 -0
  819. truthound/validators/security/redos.py +886 -0
  820. truthound/validators/security/sql_security.py +1247 -0
  821. truthound/validators/streaming/__init__.py +126 -0
  822. truthound/validators/streaming/base.py +292 -0
  823. truthound/validators/streaming/completeness.py +210 -0
  824. truthound/validators/streaming/mixin.py +575 -0
  825. truthound/validators/streaming/range.py +308 -0
  826. truthound/validators/streaming/sources.py +846 -0
  827. truthound/validators/string/__init__.py +57 -0
  828. truthound/validators/string/casing.py +158 -0
  829. truthound/validators/string/charset.py +96 -0
  830. truthound/validators/string/format.py +501 -0
  831. truthound/validators/string/json.py +77 -0
  832. truthound/validators/string/json_schema.py +184 -0
  833. truthound/validators/string/length.py +104 -0
  834. truthound/validators/string/like_pattern.py +237 -0
  835. truthound/validators/string/regex.py +202 -0
  836. truthound/validators/string/regex_extended.py +435 -0
  837. truthound/validators/table/__init__.py +88 -0
  838. truthound/validators/table/base.py +78 -0
  839. truthound/validators/table/column_count.py +198 -0
  840. truthound/validators/table/freshness.py +362 -0
  841. truthound/validators/table/row_count.py +251 -0
  842. truthound/validators/table/schema.py +333 -0
  843. truthound/validators/table/size.py +285 -0
  844. truthound/validators/timeout/__init__.py +102 -0
  845. truthound/validators/timeout/advanced/__init__.py +247 -0
  846. truthound/validators/timeout/advanced/circuit_breaker.py +675 -0
  847. truthound/validators/timeout/advanced/prediction.py +773 -0
  848. truthound/validators/timeout/advanced/priority.py +618 -0
  849. truthound/validators/timeout/advanced/redis_backend.py +770 -0
  850. truthound/validators/timeout/advanced/retry.py +721 -0
  851. truthound/validators/timeout/advanced/sampling.py +788 -0
  852. truthound/validators/timeout/advanced/sla.py +661 -0
  853. truthound/validators/timeout/advanced/telemetry.py +804 -0
  854. truthound/validators/timeout/cascade.py +477 -0
  855. truthound/validators/timeout/deadline.py +657 -0
  856. truthound/validators/timeout/degradation.py +525 -0
  857. truthound/validators/timeout/distributed.py +597 -0
  858. truthound/validators/timeseries/__init__.py +89 -0
  859. truthound/validators/timeseries/base.py +326 -0
  860. truthound/validators/timeseries/completeness.py +617 -0
  861. truthound/validators/timeseries/gap.py +485 -0
  862. truthound/validators/timeseries/monotonic.py +310 -0
  863. truthound/validators/timeseries/seasonality.py +422 -0
  864. truthound/validators/timeseries/trend.py +510 -0
  865. truthound/validators/uniqueness/__init__.py +59 -0
  866. truthound/validators/uniqueness/approximate.py +475 -0
  867. truthound/validators/uniqueness/distinct_values.py +253 -0
  868. truthound/validators/uniqueness/duplicate.py +118 -0
  869. truthound/validators/uniqueness/primary_key.py +140 -0
  870. truthound/validators/uniqueness/unique.py +191 -0
  871. truthound/validators/uniqueness/within_record.py +599 -0
  872. truthound/validators/utils.py +756 -0
  873. truthound-1.0.8.dist-info/METADATA +474 -0
  874. truthound-1.0.8.dist-info/RECORD +877 -0
  875. truthound-1.0.8.dist-info/WHEEL +4 -0
  876. truthound-1.0.8.dist-info/entry_points.txt +2 -0
  877. truthound-1.0.8.dist-info/licenses/LICENSE +190 -0
@@ -0,0 +1,1503 @@
1
+ """Enterprise structured logging system for Truthound.
2
+
3
+ This module extends the base observability logging with enterprise features:
4
+ - Correlation ID propagation across distributed systems
5
+ - Multiple log sinks (Elasticsearch, Loki, Fluentd)
6
+ - JSON structured logging for log aggregation
7
+ - Environment-aware configuration
8
+ - Async buffered logging for high throughput
9
+
10
+ Architecture:
11
+ CorrelationContext (thread-local)
12
+ |
13
+ v
14
+ EnterpriseLogger
15
+ |
16
+ +---> LogSink[] (parallel dispatch)
17
+ |
18
+ +---> ConsoleSink
19
+ +---> FileSink
20
+ +---> JsonFileSink
21
+ +---> ElasticsearchSink
22
+ +---> LokiSink
23
+ +---> FluentdSink
24
+
25
+ Usage:
26
+ >>> from truthound.infrastructure.logging import (
27
+ ... get_logger, configure_logging,
28
+ ... correlation_context, get_correlation_id,
29
+ ... )
30
+ >>>
31
+ >>> # Configure for production
32
+ >>> configure_logging(
33
+ ... environment="production",
34
+ ... format="json",
35
+ ... sinks=[
36
+ ... {"type": "console"},
37
+ ... {"type": "elasticsearch", "url": "http://elk:9200"},
38
+ ... ],
39
+ ... )
40
+ >>>
41
+ >>> # Use correlation context
42
+ >>> with correlation_context(request_id="req-123", user_id="user-456"):
43
+ ... logger = get_logger(__name__)
44
+ ... logger.info("Processing request", action="validate")
45
+ ... # All logs include request_id and user_id automatically
46
+ """
47
+
48
+ from __future__ import annotations
49
+
50
+ import asyncio
51
+ import atexit
52
+ import json
53
+ import logging
54
+ import os
55
+ import queue
56
+ import socket
57
+ import sys
58
+ import threading
59
+ import time
60
+ import traceback
61
+ import uuid
62
+ from abc import ABC, abstractmethod
63
+ from collections.abc import Mapping
64
+ from concurrent.futures import ThreadPoolExecutor
65
+ from contextlib import contextmanager
66
+ from dataclasses import dataclass, field
67
+ from datetime import datetime, timezone
68
+ from enum import IntEnum
69
+ from pathlib import Path
70
+ from typing import Any, Callable, Iterator, TextIO, TypeVar
71
+
72
+ # Re-export base logging components for compatibility
73
+ from truthound.observability.logging import (
74
+ LogLevel as BaseLogLevel,
75
+ LogRecord as BaseLogRecord,
76
+ LogContext as BaseLogContext,
77
+ )
78
+
79
+
80
+ # =============================================================================
81
+ # Log Levels (Extended)
82
+ # =============================================================================
83
+
84
+
85
+ class LogLevel(IntEnum):
86
+ """Extended log severity levels."""
87
+
88
+ TRACE = 5
89
+ DEBUG = 10
90
+ INFO = 20
91
+ WARNING = 30
92
+ ERROR = 40
93
+ CRITICAL = 50
94
+ AUDIT = 60 # Special level for audit events
95
+
96
+ @classmethod
97
+ def from_string(cls, level: str) -> "LogLevel":
98
+ """Convert string to LogLevel."""
99
+ mapping = {
100
+ "trace": cls.TRACE,
101
+ "debug": cls.DEBUG,
102
+ "info": cls.INFO,
103
+ "warning": cls.WARNING,
104
+ "warn": cls.WARNING,
105
+ "error": cls.ERROR,
106
+ "critical": cls.CRITICAL,
107
+ "fatal": cls.CRITICAL,
108
+ "audit": cls.AUDIT,
109
+ }
110
+ return mapping.get(level.lower(), cls.INFO)
111
+
112
+
113
+ # =============================================================================
114
+ # Correlation Context
115
+ # =============================================================================
116
+
117
+
118
+ class CorrelationContext:
119
+ """Thread-local correlation context for distributed tracing.
120
+
121
+ Maintains correlation IDs and contextual fields that are automatically
122
+ included in all log messages within the context.
123
+
124
+ This enables tracking requests across service boundaries and correlating
125
+ logs from different components of a distributed system.
126
+
127
+ Example:
128
+ >>> with correlation_context(
129
+ ... request_id="req-123",
130
+ ... user_id="user-456",
131
+ ... trace_id="trace-789",
132
+ ... ):
133
+ ... logger.info("Processing") # Includes all context fields
134
+ ... call_downstream_service() # Context propagates
135
+ """
136
+
137
+ _local = threading.local()
138
+ _CONTEXT_HEADER_PREFIX = "X-Correlation-"
139
+
140
+ @classmethod
141
+ def get_current(cls) -> dict[str, Any]:
142
+ """Get current context fields (merged from all levels)."""
143
+ if not hasattr(cls._local, "stack"):
144
+ cls._local.stack = [{}]
145
+ result: dict[str, Any] = {}
146
+ for ctx in cls._local.stack:
147
+ result.update(ctx)
148
+ return result
149
+
150
+ @classmethod
151
+ def get_correlation_id(cls) -> str | None:
152
+ """Get the current correlation/request ID."""
153
+ ctx = cls.get_current()
154
+ return ctx.get("correlation_id") or ctx.get("request_id")
155
+
156
+ @classmethod
157
+ def get_trace_id(cls) -> str | None:
158
+ """Get the current trace ID."""
159
+ return cls.get_current().get("trace_id")
160
+
161
+ @classmethod
162
+ def get_span_id(cls) -> str | None:
163
+ """Get the current span ID."""
164
+ return cls.get_current().get("span_id")
165
+
166
+ @classmethod
167
+ def push(cls, **fields: Any) -> None:
168
+ """Push new context level."""
169
+ if not hasattr(cls._local, "stack"):
170
+ cls._local.stack = [{}]
171
+ cls._local.stack.append(fields)
172
+
173
+ @classmethod
174
+ def pop(cls) -> dict[str, Any]:
175
+ """Pop context level."""
176
+ if hasattr(cls._local, "stack") and len(cls._local.stack) > 1:
177
+ return cls._local.stack.pop()
178
+ return {}
179
+
180
+ @classmethod
181
+ def clear(cls) -> None:
182
+ """Clear all context."""
183
+ cls._local.stack = [{}]
184
+
185
+ @classmethod
186
+ def to_headers(cls) -> dict[str, str]:
187
+ """Convert context to HTTP headers for propagation.
188
+
189
+ Returns:
190
+ Dictionary of header name to value.
191
+ """
192
+ ctx = cls.get_current()
193
+ headers = {}
194
+ for key, value in ctx.items():
195
+ header_name = f"{cls._CONTEXT_HEADER_PREFIX}{key.replace('_', '-').title()}"
196
+ headers[header_name] = str(value)
197
+ return headers
198
+
199
+ @classmethod
200
+ def from_headers(cls, headers: Mapping[str, str]) -> dict[str, Any]:
201
+ """Extract context from HTTP headers.
202
+
203
+ Args:
204
+ headers: HTTP headers mapping.
205
+
206
+ Returns:
207
+ Extracted context fields.
208
+ """
209
+ prefix = cls._CONTEXT_HEADER_PREFIX.lower()
210
+ context = {}
211
+ for key, value in headers.items():
212
+ if key.lower().startswith(prefix):
213
+ field_name = key[len(prefix) :].lower().replace("-", "_")
214
+ context[field_name] = value
215
+ return context
216
+
217
+
218
+ @contextmanager
219
+ def correlation_context(**fields: Any) -> Iterator[None]:
220
+ """Context manager for adding correlation fields.
221
+
222
+ Args:
223
+ **fields: Key-value pairs to add to context.
224
+ Common fields: request_id, correlation_id, trace_id,
225
+ span_id, user_id, session_id, tenant_id.
226
+
227
+ Example:
228
+ >>> with correlation_context(request_id="abc", user_id="123"):
229
+ ... logger.info("User action") # Includes request_id and user_id
230
+ """
231
+ # Auto-generate correlation_id if not provided and not in current context
232
+ if "correlation_id" not in fields and "request_id" not in fields:
233
+ # Check if correlation already exists in parent context
234
+ existing_cid = CorrelationContext.get_correlation_id()
235
+ if existing_cid is None:
236
+ fields["correlation_id"] = generate_correlation_id()
237
+
238
+ CorrelationContext.push(**fields)
239
+ try:
240
+ yield
241
+ finally:
242
+ CorrelationContext.pop()
243
+
244
+
245
+ def get_correlation_id() -> str | None:
246
+ """Get the current correlation ID."""
247
+ return CorrelationContext.get_correlation_id()
248
+
249
+
250
+ def set_correlation_id(correlation_id: str) -> None:
251
+ """Set correlation ID in current context.
252
+
253
+ Note: This modifies the current context level. Use correlation_context()
254
+ for proper scoping.
255
+ """
256
+ ctx = CorrelationContext.get_current()
257
+ if hasattr(CorrelationContext._local, "stack") and CorrelationContext._local.stack:
258
+ CorrelationContext._local.stack[-1]["correlation_id"] = correlation_id
259
+
260
+
261
+ def generate_correlation_id() -> str:
262
+ """Generate a unique correlation ID.
263
+
264
+ Format: {timestamp_hex}-{random_hex}
265
+ Example: 65a1b2c3-4d5e6f7a8b9c
266
+ """
267
+ timestamp = int(time.time() * 1000) & 0xFFFFFFFF
268
+ random_part = uuid.uuid4().hex[:12]
269
+ return f"{timestamp:08x}-{random_part}"
270
+
271
+
272
+ # =============================================================================
273
+ # Log Record (Extended)
274
+ # =============================================================================
275
+
276
+
277
+ @dataclass
278
+ class LogRecord:
279
+ """Extended log record with correlation and enterprise features.
280
+
281
+ Attributes:
282
+ timestamp: When the log was created (UTC).
283
+ level: Log severity level.
284
+ message: Human-readable log message.
285
+ logger_name: Name of the logger.
286
+ fields: Structured key-value data.
287
+ exception: Exception info if present.
288
+ correlation_id: Distributed correlation ID.
289
+ trace_id: Distributed trace ID.
290
+ span_id: Current span ID.
291
+ caller: Source location (file:line:function).
292
+ thread_id: Thread identifier.
293
+ process_id: Process identifier.
294
+ hostname: Machine hostname.
295
+ service: Service name.
296
+ environment: Environment name.
297
+ """
298
+
299
+ timestamp: datetime
300
+ level: LogLevel
301
+ message: str
302
+ logger_name: str
303
+ fields: dict[str, Any] = field(default_factory=dict)
304
+ exception: BaseException | None = None
305
+ correlation_id: str | None = None
306
+ trace_id: str | None = None
307
+ span_id: str | None = None
308
+ caller: str | None = None
309
+ thread_id: int = 0
310
+ thread_name: str = ""
311
+ process_id: int = 0
312
+ hostname: str = ""
313
+ service: str = ""
314
+ environment: str = ""
315
+
316
+ def __post_init__(self) -> None:
317
+ """Set process/thread info."""
318
+ if not self.thread_id:
319
+ self.thread_id = threading.current_thread().ident or 0
320
+ if not self.thread_name:
321
+ self.thread_name = threading.current_thread().name
322
+ if not self.process_id:
323
+ self.process_id = os.getpid()
324
+ if not self.hostname:
325
+ self.hostname = socket.gethostname()
326
+
327
+ def to_dict(self, include_meta: bool = True) -> dict[str, Any]:
328
+ """Convert to dictionary.
329
+
330
+ Args:
331
+ include_meta: Include metadata fields (thread, process, etc).
332
+
333
+ Returns:
334
+ Dictionary representation.
335
+ """
336
+ data = {
337
+ "timestamp": self.timestamp.isoformat(),
338
+ "@timestamp": self.timestamp.isoformat(), # ELK compatibility
339
+ "level": self.level.name.lower(),
340
+ "message": self.message,
341
+ "logger": self.logger_name,
342
+ **self.fields,
343
+ }
344
+
345
+ # Correlation fields
346
+ if self.correlation_id:
347
+ data["correlation_id"] = self.correlation_id
348
+ if self.trace_id:
349
+ data["trace_id"] = self.trace_id
350
+ if self.span_id:
351
+ data["span_id"] = self.span_id
352
+
353
+ # Location
354
+ if self.caller:
355
+ data["caller"] = self.caller
356
+
357
+ # Service info
358
+ if self.service:
359
+ data["service"] = self.service
360
+ if self.environment:
361
+ data["environment"] = self.environment
362
+
363
+ # Metadata
364
+ if include_meta:
365
+ data["thread_id"] = self.thread_id
366
+ data["thread_name"] = self.thread_name
367
+ data["process_id"] = self.process_id
368
+ data["hostname"] = self.hostname
369
+
370
+ # Exception
371
+ if self.exception:
372
+ data["exception"] = {
373
+ "type": type(self.exception).__name__,
374
+ "message": str(self.exception),
375
+ "traceback": traceback.format_exception(
376
+ type(self.exception),
377
+ self.exception,
378
+ self.exception.__traceback__,
379
+ ),
380
+ }
381
+
382
+ return data
383
+
384
+ def to_json(self, indent: int | None = None) -> str:
385
+ """Convert to JSON string."""
386
+ return json.dumps(self.to_dict(), default=str, indent=indent)
387
+
388
+ def to_logfmt(self) -> str:
389
+ """Convert to logfmt format."""
390
+ parts = [
391
+ f'ts={self.timestamp.isoformat()}',
392
+ f'level={self.level.name.lower()}',
393
+ f'msg="{self._escape(self.message)}"',
394
+ f'logger={self.logger_name}',
395
+ ]
396
+
397
+ if self.correlation_id:
398
+ parts.append(f"correlation_id={self.correlation_id}")
399
+ if self.trace_id:
400
+ parts.append(f"trace_id={self.trace_id}")
401
+
402
+ for key, value in self.fields.items():
403
+ parts.append(f"{key}={self._format_value(value)}")
404
+
405
+ return " ".join(parts)
406
+
407
+ def _escape(self, value: str) -> str:
408
+ """Escape special characters."""
409
+ return value.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
410
+
411
+ def _format_value(self, value: Any) -> str:
412
+ """Format a value for logfmt."""
413
+ if isinstance(value, bool):
414
+ return "true" if value else "false"
415
+ elif isinstance(value, (int, float)):
416
+ return str(value)
417
+ elif isinstance(value, str):
418
+ if " " in value or '"' in value or "=" in value:
419
+ return f'"{self._escape(value)}"'
420
+ return value
421
+ else:
422
+ return f'"{self._escape(str(value))}"'
423
+
424
+
425
+ # =============================================================================
426
+ # Log Sinks
427
+ # =============================================================================
428
+
429
+
430
+ class LogSink(ABC):
431
+ """Abstract base class for log output sinks.
432
+
433
+ Sinks are responsible for delivering log records to their destinations.
434
+ Multiple sinks can be configured to send logs to different systems.
435
+ """
436
+
437
+ def __init__(
438
+ self,
439
+ level: LogLevel = LogLevel.DEBUG,
440
+ filters: list[Callable[[LogRecord], bool]] | None = None,
441
+ ) -> None:
442
+ """Initialize sink.
443
+
444
+ Args:
445
+ level: Minimum log level to accept.
446
+ filters: Optional filter functions.
447
+ """
448
+ self._level = level
449
+ self._filters = filters or []
450
+ self._lock = threading.Lock()
451
+
452
+ @property
453
+ def level(self) -> LogLevel:
454
+ """Get minimum log level."""
455
+ return self._level
456
+
457
+ def should_emit(self, record: LogRecord) -> bool:
458
+ """Check if record should be emitted.
459
+
460
+ Args:
461
+ record: Log record to check.
462
+
463
+ Returns:
464
+ True if should emit.
465
+ """
466
+ if record.level < self._level:
467
+ return False
468
+ for f in self._filters:
469
+ if not f(record):
470
+ return False
471
+ return True
472
+
473
+ @abstractmethod
474
+ def emit(self, record: LogRecord) -> None:
475
+ """Emit a log record.
476
+
477
+ Args:
478
+ record: Record to emit.
479
+ """
480
+ pass
481
+
482
+ def emit_batch(self, records: list[LogRecord]) -> None:
483
+ """Emit multiple records (default: emit one by one).
484
+
485
+ Args:
486
+ records: Records to emit.
487
+ """
488
+ for record in records:
489
+ if self.should_emit(record):
490
+ self.emit(record)
491
+
492
+ def close(self) -> None:
493
+ """Clean up sink resources."""
494
+ pass
495
+
496
+ def flush(self) -> None:
497
+ """Flush any buffered records."""
498
+ pass
499
+
500
+
501
+ class ConsoleSink(LogSink):
502
+ """Console output sink with optional coloring.
503
+
504
+ Outputs logs to stdout/stderr with human-readable formatting.
505
+ """
506
+
507
+ COLORS = {
508
+ LogLevel.TRACE: "\033[90m", # Gray
509
+ LogLevel.DEBUG: "\033[36m", # Cyan
510
+ LogLevel.INFO: "\033[32m", # Green
511
+ LogLevel.WARNING: "\033[33m", # Yellow
512
+ LogLevel.ERROR: "\033[31m", # Red
513
+ LogLevel.CRITICAL: "\033[35m", # Magenta
514
+ LogLevel.AUDIT: "\033[34m", # Blue
515
+ }
516
+ RESET = "\033[0m"
517
+
518
+ def __init__(
519
+ self,
520
+ *,
521
+ stream: TextIO | None = None,
522
+ color: bool = True,
523
+ format: str = "console", # console, json, logfmt
524
+ split_stderr: bool = True,
525
+ timestamp_format: str = "%Y-%m-%d %H:%M:%S",
526
+ **kwargs: Any,
527
+ ) -> None:
528
+ """Initialize console sink.
529
+
530
+ Args:
531
+ stream: Output stream (None for auto).
532
+ color: Enable ANSI colors.
533
+ format: Output format (console, json, logfmt).
534
+ split_stderr: Send warnings+ to stderr.
535
+ timestamp_format: strftime format.
536
+ **kwargs: Arguments for LogSink.
537
+ """
538
+ super().__init__(**kwargs)
539
+ self._stream = stream
540
+ self._color = color and (stream is None or stream.isatty())
541
+ self._format = format
542
+ self._split_stderr = split_stderr
543
+ self._timestamp_format = timestamp_format
544
+
545
+ def emit(self, record: LogRecord) -> None:
546
+ """Write log to console."""
547
+ if self._format == "json":
548
+ message = record.to_json()
549
+ elif self._format == "logfmt":
550
+ message = record.to_logfmt()
551
+ else:
552
+ message = self._format_console(record)
553
+
554
+ stream = self._get_stream(record)
555
+ with self._lock:
556
+ try:
557
+ stream.write(message + "\n")
558
+ stream.flush()
559
+ except Exception:
560
+ pass
561
+
562
+ def _get_stream(self, record: LogRecord) -> TextIO:
563
+ """Get appropriate output stream."""
564
+ if self._stream:
565
+ return self._stream
566
+ if self._split_stderr and record.level >= LogLevel.WARNING:
567
+ return sys.stderr
568
+ return sys.stdout
569
+
570
+ def _format_console(self, record: LogRecord) -> str:
571
+ """Format record for console output."""
572
+ parts = []
573
+
574
+ # Timestamp
575
+ ts = record.timestamp.strftime(self._timestamp_format)
576
+ parts.append(ts)
577
+
578
+ # Level
579
+ level = record.level.name.ljust(8)
580
+ if self._color:
581
+ color = self.COLORS.get(record.level, "")
582
+ level = f"{color}{level}{self.RESET}"
583
+ parts.append(level)
584
+
585
+ # Correlation ID (short form)
586
+ if record.correlation_id:
587
+ cid = record.correlation_id[:8]
588
+ parts.append(f"[{cid}]")
589
+
590
+ # Logger name
591
+ parts.append(f"[{record.logger_name}]")
592
+
593
+ # Message
594
+ parts.append(record.message)
595
+
596
+ # Fields
597
+ if record.fields:
598
+ field_strs = [f"{k}={v}" for k, v in record.fields.items()]
599
+ parts.append(" ".join(field_strs))
600
+
601
+ result = " ".join(parts)
602
+
603
+ # Exception
604
+ if record.exception:
605
+ tb = "".join(
606
+ traceback.format_exception(
607
+ type(record.exception),
608
+ record.exception,
609
+ record.exception.__traceback__,
610
+ )
611
+ )
612
+ result = f"{result}\n{tb}"
613
+
614
+ return result
615
+
616
+
617
+ class FileSink(LogSink):
618
+ """File output sink with rotation support."""
619
+
620
+ def __init__(
621
+ self,
622
+ path: str | Path,
623
+ *,
624
+ format: str = "json",
625
+ max_bytes: int = 10 * 1024 * 1024, # 10MB
626
+ backup_count: int = 5,
627
+ encoding: str = "utf-8",
628
+ **kwargs: Any,
629
+ ) -> None:
630
+ """Initialize file sink.
631
+
632
+ Args:
633
+ path: Path to log file.
634
+ format: Output format (json, logfmt, text).
635
+ max_bytes: Max file size before rotation.
636
+ backup_count: Number of backup files.
637
+ encoding: File encoding.
638
+ **kwargs: Arguments for LogSink.
639
+ """
640
+ super().__init__(**kwargs)
641
+ self._path = Path(path)
642
+ self._format = format
643
+ self._max_bytes = max_bytes
644
+ self._backup_count = backup_count
645
+ self._encoding = encoding
646
+ self._file: TextIO | None = None
647
+
648
+ def emit(self, record: LogRecord) -> None:
649
+ """Write log to file."""
650
+ if self._format == "json":
651
+ message = record.to_json()
652
+ elif self._format == "logfmt":
653
+ message = record.to_logfmt()
654
+ else:
655
+ message = f"{record.timestamp.isoformat()} {record.level.name} [{record.logger_name}] {record.message}"
656
+
657
+ with self._lock:
658
+ try:
659
+ if self._should_rotate():
660
+ self._rotate()
661
+ f = self._ensure_file()
662
+ f.write(message + "\n")
663
+ f.flush()
664
+ except Exception:
665
+ pass
666
+
667
+ def _ensure_file(self) -> TextIO:
668
+ """Ensure file is open."""
669
+ if self._file is None:
670
+ self._path.parent.mkdir(parents=True, exist_ok=True)
671
+ self._file = open(self._path, "a", encoding=self._encoding)
672
+ return self._file
673
+
674
+ def _should_rotate(self) -> bool:
675
+ """Check if rotation is needed."""
676
+ try:
677
+ return self._path.exists() and self._path.stat().st_size >= self._max_bytes
678
+ except Exception:
679
+ return False
680
+
681
+ def _rotate(self) -> None:
682
+ """Rotate log files."""
683
+ if self._file:
684
+ self._file.close()
685
+ self._file = None
686
+
687
+ # Rotate existing backups
688
+ for i in range(self._backup_count - 1, 0, -1):
689
+ src = self._path.with_suffix(f"{self._path.suffix}.{i}")
690
+ dst = self._path.with_suffix(f"{self._path.suffix}.{i + 1}")
691
+ if src.exists():
692
+ src.rename(dst)
693
+
694
+ # Move current to .1
695
+ if self._path.exists():
696
+ self._path.rename(self._path.with_suffix(f"{self._path.suffix}.1"))
697
+
698
+ def close(self) -> None:
699
+ """Close file."""
700
+ if self._file:
701
+ try:
702
+ self._file.close()
703
+ except Exception:
704
+ pass
705
+ self._file = None
706
+
707
+
708
+ class JsonFileSink(FileSink):
709
+ """JSON file sink (convenience wrapper)."""
710
+
711
+ def __init__(self, path: str | Path, **kwargs: Any) -> None:
712
+ super().__init__(path, format="json", **kwargs)
713
+
714
+
715
+ class ElasticsearchSink(LogSink):
716
+ """Elasticsearch log sink for centralized logging.
717
+
718
+ Sends logs to Elasticsearch/OpenSearch for aggregation and search.
719
+ Supports bulk indexing for high throughput.
720
+ """
721
+
722
+ def __init__(
723
+ self,
724
+ url: str,
725
+ *,
726
+ index_prefix: str = "truthound-logs",
727
+ index_pattern: str = "daily", # daily, weekly, monthly
728
+ username: str | None = None,
729
+ password: str | None = None,
730
+ api_key: str | None = None,
731
+ batch_size: int = 100,
732
+ flush_interval: float = 5.0,
733
+ **kwargs: Any,
734
+ ) -> None:
735
+ """Initialize Elasticsearch sink.
736
+
737
+ Args:
738
+ url: Elasticsearch URL.
739
+ index_prefix: Index name prefix.
740
+ index_pattern: Index rotation pattern.
741
+ username: Basic auth username.
742
+ password: Basic auth password.
743
+ api_key: API key for auth.
744
+ batch_size: Batch size for bulk indexing.
745
+ flush_interval: Flush interval in seconds.
746
+ **kwargs: Arguments for LogSink.
747
+ """
748
+ super().__init__(**kwargs)
749
+ self._url = url.rstrip("/")
750
+ self._index_prefix = index_prefix
751
+ self._index_pattern = index_pattern
752
+ self._username = username
753
+ self._password = password
754
+ self._api_key = api_key
755
+ self._batch_size = batch_size
756
+ self._flush_interval = flush_interval
757
+
758
+ self._buffer: list[LogRecord] = []
759
+ self._last_flush = time.time()
760
+ self._executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix="es-sink")
761
+ self._running = True
762
+
763
+ # Start background flusher
764
+ self._flush_thread = threading.Thread(
765
+ target=self._background_flush,
766
+ daemon=True,
767
+ name="es-sink-flusher",
768
+ )
769
+ self._flush_thread.start()
770
+
771
+ def emit(self, record: LogRecord) -> None:
772
+ """Buffer record for bulk indexing."""
773
+ with self._lock:
774
+ self._buffer.append(record)
775
+ if len(self._buffer) >= self._batch_size:
776
+ self._flush_buffer()
777
+
778
+ def _background_flush(self) -> None:
779
+ """Background thread for periodic flushing."""
780
+ while self._running:
781
+ time.sleep(1)
782
+ with self._lock:
783
+ if (
784
+ self._buffer
785
+ and time.time() - self._last_flush >= self._flush_interval
786
+ ):
787
+ self._flush_buffer()
788
+
789
+ def _flush_buffer(self) -> None:
790
+ """Flush buffered records to Elasticsearch."""
791
+ if not self._buffer:
792
+ return
793
+
794
+ records = self._buffer.copy()
795
+ self._buffer.clear()
796
+ self._last_flush = time.time()
797
+
798
+ # Submit to executor
799
+ self._executor.submit(self._bulk_index, records)
800
+
801
+ def _get_index_name(self, timestamp: datetime) -> str:
802
+ """Get index name for timestamp."""
803
+ if self._index_pattern == "daily":
804
+ suffix = timestamp.strftime("%Y.%m.%d")
805
+ elif self._index_pattern == "weekly":
806
+ suffix = timestamp.strftime("%Y.%W")
807
+ elif self._index_pattern == "monthly":
808
+ suffix = timestamp.strftime("%Y.%m")
809
+ else:
810
+ suffix = timestamp.strftime("%Y.%m.%d")
811
+ return f"{self._index_prefix}-{suffix}"
812
+
813
+ def _bulk_index(self, records: list[LogRecord]) -> None:
814
+ """Bulk index records to Elasticsearch."""
815
+ try:
816
+ import urllib.request
817
+ import urllib.error
818
+
819
+ # Build bulk request body
820
+ lines = []
821
+ for record in records:
822
+ index_name = self._get_index_name(record.timestamp)
823
+ action = json.dumps({"index": {"_index": index_name}})
824
+ doc = json.dumps(record.to_dict(), default=str)
825
+ lines.append(action)
826
+ lines.append(doc)
827
+ body = "\n".join(lines) + "\n"
828
+
829
+ # Build request
830
+ url = f"{self._url}/_bulk"
831
+ headers = {"Content-Type": "application/x-ndjson"}
832
+
833
+ if self._api_key:
834
+ headers["Authorization"] = f"ApiKey {self._api_key}"
835
+
836
+ request = urllib.request.Request(
837
+ url,
838
+ data=body.encode("utf-8"),
839
+ headers=headers,
840
+ method="POST",
841
+ )
842
+
843
+ if self._username and self._password:
844
+ import base64
845
+
846
+ credentials = base64.b64encode(
847
+ f"{self._username}:{self._password}".encode()
848
+ ).decode()
849
+ request.add_header("Authorization", f"Basic {credentials}")
850
+
851
+ with urllib.request.urlopen(request, timeout=30):
852
+ pass
853
+
854
+ except Exception:
855
+ pass # Silently fail - don't break logging
856
+
857
+ def flush(self) -> None:
858
+ """Flush buffered records."""
859
+ with self._lock:
860
+ self._flush_buffer()
861
+
862
+ def close(self) -> None:
863
+ """Close sink."""
864
+ self._running = False
865
+ self.flush()
866
+ self._executor.shutdown(wait=True)
867
+
868
+
869
+ class LokiSink(LogSink):
870
+ """Grafana Loki log sink.
871
+
872
+ Sends logs to Loki for aggregation with Prometheus-style labels.
873
+ """
874
+
875
+ def __init__(
876
+ self,
877
+ url: str,
878
+ *,
879
+ labels: dict[str, str] | None = None,
880
+ batch_size: int = 100,
881
+ flush_interval: float = 5.0,
882
+ **kwargs: Any,
883
+ ) -> None:
884
+ """Initialize Loki sink.
885
+
886
+ Args:
887
+ url: Loki push URL (e.g., http://loki:3100/loki/api/v1/push).
888
+ labels: Static labels to add to all logs.
889
+ batch_size: Batch size.
890
+ flush_interval: Flush interval in seconds.
891
+ **kwargs: Arguments for LogSink.
892
+ """
893
+ super().__init__(**kwargs)
894
+ self._url = url
895
+ self._labels = labels or {}
896
+ self._batch_size = batch_size
897
+ self._flush_interval = flush_interval
898
+ self._buffer: list[LogRecord] = []
899
+ self._last_flush = time.time()
900
+ self._executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix="loki-sink")
901
+
902
+ def emit(self, record: LogRecord) -> None:
903
+ """Buffer record for batch push."""
904
+ with self._lock:
905
+ self._buffer.append(record)
906
+ if len(self._buffer) >= self._batch_size:
907
+ self._flush_buffer()
908
+
909
+ def _flush_buffer(self) -> None:
910
+ """Flush buffered records to Loki."""
911
+ if not self._buffer:
912
+ return
913
+
914
+ records = self._buffer.copy()
915
+ self._buffer.clear()
916
+ self._last_flush = time.time()
917
+ self._executor.submit(self._push_to_loki, records)
918
+
919
+ def _push_to_loki(self, records: list[LogRecord]) -> None:
920
+ """Push records to Loki."""
921
+ try:
922
+ import urllib.request
923
+
924
+ # Group by labels
925
+ streams: dict[str, list[tuple[str, str]]] = {}
926
+ for record in records:
927
+ labels = {
928
+ **self._labels,
929
+ "level": record.level.name.lower(),
930
+ "logger": record.logger_name,
931
+ }
932
+ if record.service:
933
+ labels["service"] = record.service
934
+ if record.environment:
935
+ labels["environment"] = record.environment
936
+
937
+ label_str = "{" + ",".join(f'{k}="{v}"' for k, v in sorted(labels.items())) + "}"
938
+ if label_str not in streams:
939
+ streams[label_str] = []
940
+
941
+ # Loki expects nanosecond timestamps
942
+ ts_ns = str(int(record.timestamp.timestamp() * 1_000_000_000))
943
+ streams[label_str].append([ts_ns, record.to_json()])
944
+
945
+ # Build Loki push format
946
+ payload = {
947
+ "streams": [
948
+ {"stream": json.loads(label_str.replace("=", ":")), "values": values}
949
+ for label_str, values in streams.items()
950
+ ]
951
+ }
952
+
953
+ request = urllib.request.Request(
954
+ self._url,
955
+ data=json.dumps(payload).encode("utf-8"),
956
+ headers={"Content-Type": "application/json"},
957
+ method="POST",
958
+ )
959
+
960
+ with urllib.request.urlopen(request, timeout=30):
961
+ pass
962
+
963
+ except Exception:
964
+ pass
965
+
966
+ def flush(self) -> None:
967
+ """Flush buffered records."""
968
+ with self._lock:
969
+ self._flush_buffer()
970
+
971
+ def close(self) -> None:
972
+ """Close sink."""
973
+ self.flush()
974
+ self._executor.shutdown(wait=True)
975
+
976
+
977
+ class FluentdSink(LogSink):
978
+ """Fluentd log sink.
979
+
980
+ Sends logs to Fluentd using the Forward protocol.
981
+ """
982
+
983
+ def __init__(
984
+ self,
985
+ host: str = "localhost",
986
+ port: int = 24224,
987
+ *,
988
+ tag: str = "truthound",
989
+ **kwargs: Any,
990
+ ) -> None:
991
+ """Initialize Fluentd sink.
992
+
993
+ Args:
994
+ host: Fluentd host.
995
+ port: Fluentd port.
996
+ tag: Fluentd tag prefix.
997
+ **kwargs: Arguments for LogSink.
998
+ """
999
+ super().__init__(**kwargs)
1000
+ self._host = host
1001
+ self._port = port
1002
+ self._tag = tag
1003
+ self._socket: socket.socket | None = None
1004
+
1005
+ def emit(self, record: LogRecord) -> None:
1006
+ """Send record to Fluentd."""
1007
+ try:
1008
+ import msgpack # type: ignore
1009
+ except ImportError:
1010
+ # Fallback to JSON
1011
+ self._emit_json(record)
1012
+ return
1013
+
1014
+ try:
1015
+ sock = self._get_socket()
1016
+ tag = f"{self._tag}.{record.level.name.lower()}"
1017
+ timestamp = int(record.timestamp.timestamp())
1018
+ data = record.to_dict(include_meta=True)
1019
+
1020
+ # Forward protocol: [tag, time, record]
1021
+ message = msgpack.packb([tag, timestamp, data])
1022
+ sock.sendall(message)
1023
+
1024
+ except Exception:
1025
+ self._socket = None # Reset socket on error
1026
+
1027
+ def _emit_json(self, record: LogRecord) -> None:
1028
+ """Emit using JSON (fallback)."""
1029
+ try:
1030
+ sock = self._get_socket()
1031
+ data = {
1032
+ "tag": f"{self._tag}.{record.level.name.lower()}",
1033
+ "time": int(record.timestamp.timestamp()),
1034
+ "record": record.to_dict(include_meta=True),
1035
+ }
1036
+ message = json.dumps(data).encode("utf-8") + b"\n"
1037
+ sock.sendall(message)
1038
+ except Exception:
1039
+ self._socket = None
1040
+
1041
+ def _get_socket(self) -> socket.socket:
1042
+ """Get or create socket."""
1043
+ if self._socket is None:
1044
+ self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1045
+ self._socket.connect((self._host, self._port))
1046
+ return self._socket
1047
+
1048
+ def close(self) -> None:
1049
+ """Close socket."""
1050
+ if self._socket:
1051
+ try:
1052
+ self._socket.close()
1053
+ except Exception:
1054
+ pass
1055
+ self._socket = None
1056
+
1057
+
1058
+ # =============================================================================
1059
+ # Log Configuration
1060
+ # =============================================================================
1061
+
1062
+
1063
+ @dataclass
1064
+ class LogConfig:
1065
+ """Logging configuration.
1066
+
1067
+ Example:
1068
+ >>> config = LogConfig(
1069
+ ... level="info",
1070
+ ... format="json",
1071
+ ... service="truthound",
1072
+ ... environment="production",
1073
+ ... sinks=[
1074
+ ... {"type": "console"},
1075
+ ... {"type": "file", "path": "/var/log/truthound.log"},
1076
+ ... {"type": "elasticsearch", "url": "http://elk:9200"},
1077
+ ... ],
1078
+ ... )
1079
+ """
1080
+
1081
+ level: str | LogLevel = LogLevel.INFO
1082
+ format: str = "console" # console, json, logfmt
1083
+ service: str = ""
1084
+ environment: str = ""
1085
+ include_caller: bool = False
1086
+ include_meta: bool = True
1087
+
1088
+ # Sinks configuration
1089
+ sinks: list[dict[str, Any]] = field(default_factory=lambda: [{"type": "console"}])
1090
+
1091
+ # Buffering
1092
+ async_logging: bool = True
1093
+ buffer_size: int = 1000
1094
+ flush_interval: float = 1.0
1095
+
1096
+ @classmethod
1097
+ def development(cls) -> "LogConfig":
1098
+ """Development configuration."""
1099
+ return cls(
1100
+ level=LogLevel.DEBUG,
1101
+ format="console",
1102
+ environment="development",
1103
+ include_caller=True,
1104
+ async_logging=False,
1105
+ )
1106
+
1107
+ @classmethod
1108
+ def production(cls, service: str) -> "LogConfig":
1109
+ """Production configuration."""
1110
+ return cls(
1111
+ level=LogLevel.INFO,
1112
+ format="json",
1113
+ service=service,
1114
+ environment="production",
1115
+ include_caller=False,
1116
+ async_logging=True,
1117
+ )
1118
+
1119
+ @classmethod
1120
+ def from_environment(cls) -> "LogConfig":
1121
+ """Load configuration from environment variables."""
1122
+ return cls(
1123
+ level=os.getenv("LOG_LEVEL", "INFO"),
1124
+ format=os.getenv("LOG_FORMAT", "console"),
1125
+ service=os.getenv("SERVICE_NAME", ""),
1126
+ environment=os.getenv("ENVIRONMENT", "development"),
1127
+ include_caller=os.getenv("LOG_INCLUDE_CALLER", "").lower() == "true",
1128
+ )
1129
+
1130
+
1131
+ # =============================================================================
1132
+ # Enterprise Logger
1133
+ # =============================================================================
1134
+
1135
+
1136
+ class EnterpriseLogger:
1137
+ """Enterprise-grade structured logger.
1138
+
1139
+ Features:
1140
+ - Automatic correlation ID propagation
1141
+ - Multiple output sinks
1142
+ - Async buffered logging
1143
+ - Environment-aware configuration
1144
+ - Field binding for contextual logging
1145
+
1146
+ Example:
1147
+ >>> logger = EnterpriseLogger("my.module", config=LogConfig.production("my-service"))
1148
+ >>> logger.info("Request received", path="/api/users", method="GET")
1149
+ >>>
1150
+ >>> # Bind fields for reuse
1151
+ >>> req_logger = logger.bind(request_id="abc123", user_id="user-456")
1152
+ >>> req_logger.info("Processing") # Includes request_id and user_id
1153
+ """
1154
+
1155
+ def __init__(
1156
+ self,
1157
+ name: str,
1158
+ *,
1159
+ config: LogConfig | None = None,
1160
+ sinks: list[LogSink] | None = None,
1161
+ ) -> None:
1162
+ """Initialize logger.
1163
+
1164
+ Args:
1165
+ name: Logger name (usually module name).
1166
+ config: Logging configuration.
1167
+ sinks: Direct sink instances (overrides config.sinks).
1168
+ """
1169
+ self._name = name
1170
+ self._config = config or LogConfig()
1171
+ self._level = (
1172
+ LogLevel.from_string(self._config.level)
1173
+ if isinstance(self._config.level, str)
1174
+ else self._config.level
1175
+ )
1176
+ self._bound_fields: dict[str, Any] = {}
1177
+
1178
+ # Initialize sinks
1179
+ if sinks:
1180
+ self._sinks = sinks
1181
+ else:
1182
+ self._sinks = self._create_sinks_from_config()
1183
+
1184
+ # Async buffer
1185
+ self._buffer: queue.Queue[LogRecord] = queue.Queue(
1186
+ maxsize=self._config.buffer_size
1187
+ )
1188
+ self._running = True
1189
+
1190
+ if self._config.async_logging:
1191
+ self._worker = threading.Thread(
1192
+ target=self._process_buffer,
1193
+ daemon=True,
1194
+ name=f"logger-{name}",
1195
+ )
1196
+ self._worker.start()
1197
+
1198
+ # Register cleanup
1199
+ atexit.register(self.close)
1200
+
1201
+ def _create_sinks_from_config(self) -> list[LogSink]:
1202
+ """Create sinks from configuration."""
1203
+ sinks = []
1204
+ for sink_config in self._config.sinks:
1205
+ sink_type = sink_config.get("type", "console")
1206
+ sink = self._create_sink(sink_type, sink_config)
1207
+ if sink:
1208
+ sinks.append(sink)
1209
+ return sinks or [ConsoleSink(format=self._config.format)]
1210
+
1211
+ def _create_sink(
1212
+ self, sink_type: str, config: dict[str, Any]
1213
+ ) -> LogSink | None:
1214
+ """Create a sink from configuration."""
1215
+ config = config.copy()
1216
+ config.pop("type", None)
1217
+
1218
+ if sink_type == "console":
1219
+ return ConsoleSink(format=self._config.format, **config)
1220
+ elif sink_type == "file":
1221
+ return FileSink(**config)
1222
+ elif sink_type == "json_file":
1223
+ return JsonFileSink(**config)
1224
+ elif sink_type == "elasticsearch":
1225
+ return ElasticsearchSink(**config)
1226
+ elif sink_type == "loki":
1227
+ return LokiSink(**config)
1228
+ elif sink_type == "fluentd":
1229
+ return FluentdSink(**config)
1230
+ else:
1231
+ return None
1232
+
1233
+ @property
1234
+ def name(self) -> str:
1235
+ """Get logger name."""
1236
+ return self._name
1237
+
1238
+ @property
1239
+ def level(self) -> LogLevel:
1240
+ """Get log level."""
1241
+ return self._level
1242
+
1243
+ @level.setter
1244
+ def level(self, value: LogLevel) -> None:
1245
+ """Set log level."""
1246
+ self._level = value
1247
+
1248
+ def bind(self, **fields: Any) -> "EnterpriseLogger":
1249
+ """Create child logger with bound fields.
1250
+
1251
+ Args:
1252
+ **fields: Fields to bind.
1253
+
1254
+ Returns:
1255
+ New logger with bound fields.
1256
+ """
1257
+ child = EnterpriseLogger(
1258
+ self._name,
1259
+ config=self._config,
1260
+ sinks=self._sinks,
1261
+ )
1262
+ child._bound_fields = {**self._bound_fields, **fields}
1263
+ child._running = self._running
1264
+ child._buffer = self._buffer # Share buffer
1265
+ return child
1266
+
1267
+ def _get_caller(self) -> str | None:
1268
+ """Get caller location."""
1269
+ if not self._config.include_caller:
1270
+ return None
1271
+
1272
+ import inspect
1273
+
1274
+ frame = inspect.currentframe()
1275
+ if frame:
1276
+ for _ in range(5): # Skip internal frames
1277
+ if frame.f_back:
1278
+ frame = frame.f_back
1279
+ filename = os.path.basename(frame.f_code.co_filename)
1280
+ return f"{filename}:{frame.f_lineno}:{frame.f_code.co_name}"
1281
+ return None
1282
+
1283
+ def _log(
1284
+ self,
1285
+ level: LogLevel,
1286
+ message: str,
1287
+ exception: BaseException | None = None,
1288
+ **fields: Any,
1289
+ ) -> None:
1290
+ """Internal log method."""
1291
+ if level < self._level:
1292
+ return
1293
+
1294
+ # Get correlation context
1295
+ ctx = CorrelationContext.get_current()
1296
+
1297
+ # Merge fields: bound -> context -> call-time
1298
+ merged_fields = {
1299
+ **self._bound_fields,
1300
+ **ctx,
1301
+ **fields,
1302
+ }
1303
+
1304
+ # Extract special fields
1305
+ correlation_id = merged_fields.pop("correlation_id", None)
1306
+ trace_id = merged_fields.pop("trace_id", None)
1307
+ span_id = merged_fields.pop("span_id", None)
1308
+
1309
+ record = LogRecord(
1310
+ timestamp=datetime.now(timezone.utc),
1311
+ level=level,
1312
+ message=message,
1313
+ logger_name=self._name,
1314
+ fields=merged_fields,
1315
+ exception=exception,
1316
+ correlation_id=correlation_id,
1317
+ trace_id=trace_id,
1318
+ span_id=span_id,
1319
+ caller=self._get_caller(),
1320
+ service=self._config.service,
1321
+ environment=self._config.environment,
1322
+ )
1323
+
1324
+ if self._config.async_logging:
1325
+ try:
1326
+ self._buffer.put_nowait(record)
1327
+ except queue.Full:
1328
+ # Buffer full, emit directly
1329
+ self._emit_record(record)
1330
+ else:
1331
+ self._emit_record(record)
1332
+
1333
+ def _emit_record(self, record: LogRecord) -> None:
1334
+ """Emit record to all sinks."""
1335
+ for sink in self._sinks:
1336
+ try:
1337
+ if sink.should_emit(record):
1338
+ sink.emit(record)
1339
+ except Exception:
1340
+ pass
1341
+
1342
+ def _process_buffer(self) -> None:
1343
+ """Process buffered records."""
1344
+ while self._running:
1345
+ try:
1346
+ record = self._buffer.get(timeout=0.1)
1347
+ self._emit_record(record)
1348
+ except queue.Empty:
1349
+ pass
1350
+ except Exception:
1351
+ pass
1352
+
1353
+ # Drain remaining records
1354
+ while not self._buffer.empty():
1355
+ try:
1356
+ record = self._buffer.get_nowait()
1357
+ self._emit_record(record)
1358
+ except queue.Empty:
1359
+ break
1360
+
1361
+ def trace(self, message: str, **fields: Any) -> None:
1362
+ """Log at TRACE level."""
1363
+ self._log(LogLevel.TRACE, message, **fields)
1364
+
1365
+ def debug(self, message: str, **fields: Any) -> None:
1366
+ """Log at DEBUG level."""
1367
+ self._log(LogLevel.DEBUG, message, **fields)
1368
+
1369
+ def info(self, message: str, **fields: Any) -> None:
1370
+ """Log at INFO level."""
1371
+ self._log(LogLevel.INFO, message, **fields)
1372
+
1373
+ def warning(self, message: str, **fields: Any) -> None:
1374
+ """Log at WARNING level."""
1375
+ self._log(LogLevel.WARNING, message, **fields)
1376
+
1377
+ def warn(self, message: str, **fields: Any) -> None:
1378
+ """Alias for warning."""
1379
+ self.warning(message, **fields)
1380
+
1381
+ def error(self, message: str, **fields: Any) -> None:
1382
+ """Log at ERROR level."""
1383
+ self._log(LogLevel.ERROR, message, **fields)
1384
+
1385
+ def critical(self, message: str, **fields: Any) -> None:
1386
+ """Log at CRITICAL level."""
1387
+ self._log(LogLevel.CRITICAL, message, **fields)
1388
+
1389
+ def fatal(self, message: str, **fields: Any) -> None:
1390
+ """Alias for critical."""
1391
+ self.critical(message, **fields)
1392
+
1393
+ def exception(
1394
+ self,
1395
+ message: str,
1396
+ exc: BaseException | None = None,
1397
+ **fields: Any,
1398
+ ) -> None:
1399
+ """Log exception with traceback."""
1400
+ if exc is None:
1401
+ exc = sys.exc_info()[1]
1402
+ self._log(LogLevel.ERROR, message, exception=exc, **fields)
1403
+
1404
+ def audit(self, message: str, **fields: Any) -> None:
1405
+ """Log audit event (special level)."""
1406
+ self._log(LogLevel.AUDIT, message, **fields)
1407
+
1408
+ def flush(self) -> None:
1409
+ """Flush all sinks."""
1410
+ # Wait for buffer to drain
1411
+ while not self._buffer.empty():
1412
+ time.sleep(0.01)
1413
+
1414
+ for sink in self._sinks:
1415
+ try:
1416
+ sink.flush()
1417
+ except Exception:
1418
+ pass
1419
+
1420
+ def close(self) -> None:
1421
+ """Close logger and all sinks."""
1422
+ self._running = False
1423
+ self.flush()
1424
+
1425
+ for sink in self._sinks:
1426
+ try:
1427
+ sink.close()
1428
+ except Exception:
1429
+ pass
1430
+
1431
+
1432
+ # =============================================================================
1433
+ # Global Logger Management
1434
+ # =============================================================================
1435
+
1436
+ _loggers: dict[str, EnterpriseLogger] = {}
1437
+ _global_config: LogConfig | None = None
1438
+ _lock = threading.Lock()
1439
+
1440
+
1441
+ def configure_logging(
1442
+ *,
1443
+ level: str | LogLevel = LogLevel.INFO,
1444
+ format: str = "console",
1445
+ service: str = "",
1446
+ environment: str = "",
1447
+ sinks: list[dict[str, Any]] | None = None,
1448
+ **kwargs: Any,
1449
+ ) -> None:
1450
+ """Configure global logging.
1451
+
1452
+ Args:
1453
+ level: Log level.
1454
+ format: Output format (console, json, logfmt).
1455
+ service: Service name.
1456
+ environment: Environment name.
1457
+ sinks: Sink configurations.
1458
+ **kwargs: Additional LogConfig parameters.
1459
+ """
1460
+ global _global_config, _loggers
1461
+
1462
+ with _lock:
1463
+ _global_config = LogConfig(
1464
+ level=level,
1465
+ format=format,
1466
+ service=service,
1467
+ environment=environment,
1468
+ sinks=sinks or [{"type": "console"}],
1469
+ **kwargs,
1470
+ )
1471
+ # Clear existing loggers so they pick up new config
1472
+ for logger in _loggers.values():
1473
+ logger.close()
1474
+ _loggers.clear()
1475
+
1476
+
1477
+ def get_logger(name: str) -> EnterpriseLogger:
1478
+ """Get or create a logger.
1479
+
1480
+ Args:
1481
+ name: Logger name (usually __name__).
1482
+
1483
+ Returns:
1484
+ EnterpriseLogger instance.
1485
+ """
1486
+ global _loggers, _global_config
1487
+
1488
+ with _lock:
1489
+ if name not in _loggers:
1490
+ config = _global_config or LogConfig.from_environment()
1491
+ _loggers[name] = EnterpriseLogger(name, config=config)
1492
+ return _loggers[name]
1493
+
1494
+
1495
+ def reset_logging() -> None:
1496
+ """Reset logging to defaults."""
1497
+ global _loggers, _global_config
1498
+
1499
+ with _lock:
1500
+ for logger in _loggers.values():
1501
+ logger.close()
1502
+ _loggers.clear()
1503
+ _global_config = None