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.
- truthound/__init__.py +162 -0
- truthound/adapters.py +100 -0
- truthound/api.py +365 -0
- truthound/audit/__init__.py +248 -0
- truthound/audit/core.py +967 -0
- truthound/audit/filters.py +620 -0
- truthound/audit/formatters.py +707 -0
- truthound/audit/logger.py +902 -0
- truthound/audit/middleware.py +571 -0
- truthound/audit/storage.py +1083 -0
- truthound/benchmark/__init__.py +123 -0
- truthound/benchmark/base.py +757 -0
- truthound/benchmark/comparison.py +635 -0
- truthound/benchmark/generators.py +706 -0
- truthound/benchmark/reporters.py +718 -0
- truthound/benchmark/runner.py +635 -0
- truthound/benchmark/scenarios.py +712 -0
- truthound/cache.py +252 -0
- truthound/checkpoint/__init__.py +136 -0
- truthound/checkpoint/actions/__init__.py +164 -0
- truthound/checkpoint/actions/base.py +324 -0
- truthound/checkpoint/actions/custom.py +234 -0
- truthound/checkpoint/actions/discord_notify.py +290 -0
- truthound/checkpoint/actions/email_notify.py +405 -0
- truthound/checkpoint/actions/github_action.py +406 -0
- truthound/checkpoint/actions/opsgenie.py +1499 -0
- truthound/checkpoint/actions/pagerduty.py +226 -0
- truthound/checkpoint/actions/slack_notify.py +233 -0
- truthound/checkpoint/actions/store_result.py +249 -0
- truthound/checkpoint/actions/teams_notify.py +1570 -0
- truthound/checkpoint/actions/telegram_notify.py +419 -0
- truthound/checkpoint/actions/update_docs.py +552 -0
- truthound/checkpoint/actions/webhook.py +293 -0
- truthound/checkpoint/analytics/__init__.py +147 -0
- truthound/checkpoint/analytics/aggregations/__init__.py +23 -0
- truthound/checkpoint/analytics/aggregations/rollup.py +481 -0
- truthound/checkpoint/analytics/aggregations/time_bucket.py +306 -0
- truthound/checkpoint/analytics/analyzers/__init__.py +17 -0
- truthound/checkpoint/analytics/analyzers/anomaly.py +386 -0
- truthound/checkpoint/analytics/analyzers/base.py +270 -0
- truthound/checkpoint/analytics/analyzers/forecast.py +421 -0
- truthound/checkpoint/analytics/analyzers/trend.py +314 -0
- truthound/checkpoint/analytics/models.py +292 -0
- truthound/checkpoint/analytics/protocols.py +549 -0
- truthound/checkpoint/analytics/service.py +718 -0
- truthound/checkpoint/analytics/stores/__init__.py +16 -0
- truthound/checkpoint/analytics/stores/base.py +306 -0
- truthound/checkpoint/analytics/stores/memory_store.py +353 -0
- truthound/checkpoint/analytics/stores/sqlite_store.py +557 -0
- truthound/checkpoint/analytics/stores/timescale_store.py +501 -0
- truthound/checkpoint/async_actions.py +794 -0
- truthound/checkpoint/async_base.py +708 -0
- truthound/checkpoint/async_checkpoint.py +617 -0
- truthound/checkpoint/async_runner.py +639 -0
- truthound/checkpoint/checkpoint.py +527 -0
- truthound/checkpoint/ci/__init__.py +61 -0
- truthound/checkpoint/ci/detector.py +355 -0
- truthound/checkpoint/ci/reporter.py +436 -0
- truthound/checkpoint/ci/templates.py +454 -0
- truthound/checkpoint/circuitbreaker/__init__.py +133 -0
- truthound/checkpoint/circuitbreaker/breaker.py +542 -0
- truthound/checkpoint/circuitbreaker/core.py +252 -0
- truthound/checkpoint/circuitbreaker/detection.py +459 -0
- truthound/checkpoint/circuitbreaker/middleware.py +389 -0
- truthound/checkpoint/circuitbreaker/registry.py +357 -0
- truthound/checkpoint/distributed/__init__.py +139 -0
- truthound/checkpoint/distributed/backends/__init__.py +35 -0
- truthound/checkpoint/distributed/backends/celery_backend.py +503 -0
- truthound/checkpoint/distributed/backends/kubernetes_backend.py +696 -0
- truthound/checkpoint/distributed/backends/local_backend.py +397 -0
- truthound/checkpoint/distributed/backends/ray_backend.py +625 -0
- truthound/checkpoint/distributed/base.py +774 -0
- truthound/checkpoint/distributed/orchestrator.py +765 -0
- truthound/checkpoint/distributed/protocols.py +842 -0
- truthound/checkpoint/distributed/registry.py +449 -0
- truthound/checkpoint/idempotency/__init__.py +120 -0
- truthound/checkpoint/idempotency/core.py +295 -0
- truthound/checkpoint/idempotency/fingerprint.py +454 -0
- truthound/checkpoint/idempotency/locking.py +604 -0
- truthound/checkpoint/idempotency/service.py +592 -0
- truthound/checkpoint/idempotency/stores.py +653 -0
- truthound/checkpoint/monitoring/__init__.py +134 -0
- truthound/checkpoint/monitoring/aggregators/__init__.py +15 -0
- truthound/checkpoint/monitoring/aggregators/base.py +372 -0
- truthound/checkpoint/monitoring/aggregators/realtime.py +300 -0
- truthound/checkpoint/monitoring/aggregators/window.py +493 -0
- truthound/checkpoint/monitoring/collectors/__init__.py +17 -0
- truthound/checkpoint/monitoring/collectors/base.py +257 -0
- truthound/checkpoint/monitoring/collectors/memory_collector.py +617 -0
- truthound/checkpoint/monitoring/collectors/prometheus_collector.py +451 -0
- truthound/checkpoint/monitoring/collectors/redis_collector.py +518 -0
- truthound/checkpoint/monitoring/events.py +410 -0
- truthound/checkpoint/monitoring/protocols.py +636 -0
- truthound/checkpoint/monitoring/service.py +578 -0
- truthound/checkpoint/monitoring/views/__init__.py +17 -0
- truthound/checkpoint/monitoring/views/base.py +172 -0
- truthound/checkpoint/monitoring/views/queue_view.py +220 -0
- truthound/checkpoint/monitoring/views/task_view.py +240 -0
- truthound/checkpoint/monitoring/views/worker_view.py +263 -0
- truthound/checkpoint/registry.py +337 -0
- truthound/checkpoint/runner.py +356 -0
- truthound/checkpoint/transaction/__init__.py +133 -0
- truthound/checkpoint/transaction/base.py +389 -0
- truthound/checkpoint/transaction/compensatable.py +537 -0
- truthound/checkpoint/transaction/coordinator.py +576 -0
- truthound/checkpoint/transaction/executor.py +622 -0
- truthound/checkpoint/transaction/idempotency.py +534 -0
- truthound/checkpoint/transaction/saga/__init__.py +143 -0
- truthound/checkpoint/transaction/saga/builder.py +584 -0
- truthound/checkpoint/transaction/saga/definition.py +515 -0
- truthound/checkpoint/transaction/saga/event_store.py +542 -0
- truthound/checkpoint/transaction/saga/patterns.py +833 -0
- truthound/checkpoint/transaction/saga/runner.py +718 -0
- truthound/checkpoint/transaction/saga/state_machine.py +793 -0
- truthound/checkpoint/transaction/saga/strategies.py +780 -0
- truthound/checkpoint/transaction/saga/testing.py +886 -0
- truthound/checkpoint/triggers/__init__.py +58 -0
- truthound/checkpoint/triggers/base.py +237 -0
- truthound/checkpoint/triggers/event.py +385 -0
- truthound/checkpoint/triggers/schedule.py +355 -0
- truthound/cli.py +2358 -0
- truthound/cli_modules/__init__.py +124 -0
- truthound/cli_modules/advanced/__init__.py +45 -0
- truthound/cli_modules/advanced/benchmark.py +343 -0
- truthound/cli_modules/advanced/docs.py +225 -0
- truthound/cli_modules/advanced/lineage.py +209 -0
- truthound/cli_modules/advanced/ml.py +320 -0
- truthound/cli_modules/advanced/realtime.py +196 -0
- truthound/cli_modules/checkpoint/__init__.py +46 -0
- truthound/cli_modules/checkpoint/init.py +114 -0
- truthound/cli_modules/checkpoint/list.py +71 -0
- truthound/cli_modules/checkpoint/run.py +159 -0
- truthound/cli_modules/checkpoint/validate.py +67 -0
- truthound/cli_modules/common/__init__.py +71 -0
- truthound/cli_modules/common/errors.py +414 -0
- truthound/cli_modules/common/options.py +419 -0
- truthound/cli_modules/common/output.py +507 -0
- truthound/cli_modules/common/protocol.py +552 -0
- truthound/cli_modules/core/__init__.py +48 -0
- truthound/cli_modules/core/check.py +123 -0
- truthound/cli_modules/core/compare.py +104 -0
- truthound/cli_modules/core/learn.py +57 -0
- truthound/cli_modules/core/mask.py +77 -0
- truthound/cli_modules/core/profile.py +65 -0
- truthound/cli_modules/core/scan.py +61 -0
- truthound/cli_modules/profiler/__init__.py +51 -0
- truthound/cli_modules/profiler/auto_profile.py +175 -0
- truthound/cli_modules/profiler/metadata.py +107 -0
- truthound/cli_modules/profiler/suite.py +283 -0
- truthound/cli_modules/registry.py +431 -0
- truthound/cli_modules/scaffolding/__init__.py +89 -0
- truthound/cli_modules/scaffolding/base.py +631 -0
- truthound/cli_modules/scaffolding/commands.py +545 -0
- truthound/cli_modules/scaffolding/plugins.py +1072 -0
- truthound/cli_modules/scaffolding/reporters.py +594 -0
- truthound/cli_modules/scaffolding/validators.py +1127 -0
- truthound/common/__init__.py +18 -0
- truthound/common/resilience/__init__.py +130 -0
- truthound/common/resilience/bulkhead.py +266 -0
- truthound/common/resilience/circuit_breaker.py +516 -0
- truthound/common/resilience/composite.py +332 -0
- truthound/common/resilience/config.py +292 -0
- truthound/common/resilience/protocols.py +217 -0
- truthound/common/resilience/rate_limiter.py +404 -0
- truthound/common/resilience/retry.py +341 -0
- truthound/datadocs/__init__.py +260 -0
- truthound/datadocs/base.py +571 -0
- truthound/datadocs/builder.py +761 -0
- truthound/datadocs/charts.py +764 -0
- truthound/datadocs/dashboard/__init__.py +63 -0
- truthound/datadocs/dashboard/app.py +576 -0
- truthound/datadocs/dashboard/components.py +584 -0
- truthound/datadocs/dashboard/state.py +240 -0
- truthound/datadocs/engine/__init__.py +46 -0
- truthound/datadocs/engine/context.py +376 -0
- truthound/datadocs/engine/pipeline.py +618 -0
- truthound/datadocs/engine/registry.py +469 -0
- truthound/datadocs/exporters/__init__.py +49 -0
- truthound/datadocs/exporters/base.py +198 -0
- truthound/datadocs/exporters/html.py +178 -0
- truthound/datadocs/exporters/json_exporter.py +253 -0
- truthound/datadocs/exporters/markdown.py +284 -0
- truthound/datadocs/exporters/pdf.py +392 -0
- truthound/datadocs/i18n/__init__.py +86 -0
- truthound/datadocs/i18n/catalog.py +960 -0
- truthound/datadocs/i18n/formatting.py +505 -0
- truthound/datadocs/i18n/loader.py +256 -0
- truthound/datadocs/i18n/plurals.py +378 -0
- truthound/datadocs/renderers/__init__.py +42 -0
- truthound/datadocs/renderers/base.py +401 -0
- truthound/datadocs/renderers/custom.py +342 -0
- truthound/datadocs/renderers/jinja.py +697 -0
- truthound/datadocs/sections.py +736 -0
- truthound/datadocs/styles.py +931 -0
- truthound/datadocs/themes/__init__.py +101 -0
- truthound/datadocs/themes/base.py +336 -0
- truthound/datadocs/themes/default.py +417 -0
- truthound/datadocs/themes/enterprise.py +419 -0
- truthound/datadocs/themes/loader.py +336 -0
- truthound/datadocs/themes.py +301 -0
- truthound/datadocs/transformers/__init__.py +57 -0
- truthound/datadocs/transformers/base.py +268 -0
- truthound/datadocs/transformers/enrichers.py +544 -0
- truthound/datadocs/transformers/filters.py +447 -0
- truthound/datadocs/transformers/i18n.py +468 -0
- truthound/datadocs/versioning/__init__.py +62 -0
- truthound/datadocs/versioning/diff.py +639 -0
- truthound/datadocs/versioning/storage.py +497 -0
- truthound/datadocs/versioning/version.py +358 -0
- truthound/datasources/__init__.py +223 -0
- truthound/datasources/_async_protocols.py +222 -0
- truthound/datasources/_protocols.py +159 -0
- truthound/datasources/adapters.py +428 -0
- truthound/datasources/async_base.py +599 -0
- truthound/datasources/async_factory.py +511 -0
- truthound/datasources/base.py +516 -0
- truthound/datasources/factory.py +433 -0
- truthound/datasources/nosql/__init__.py +47 -0
- truthound/datasources/nosql/base.py +487 -0
- truthound/datasources/nosql/elasticsearch.py +801 -0
- truthound/datasources/nosql/mongodb.py +636 -0
- truthound/datasources/pandas_optimized.py +582 -0
- truthound/datasources/pandas_source.py +216 -0
- truthound/datasources/polars_source.py +395 -0
- truthound/datasources/spark_source.py +479 -0
- truthound/datasources/sql/__init__.py +154 -0
- truthound/datasources/sql/base.py +710 -0
- truthound/datasources/sql/bigquery.py +410 -0
- truthound/datasources/sql/cloud_base.py +199 -0
- truthound/datasources/sql/databricks.py +471 -0
- truthound/datasources/sql/mysql.py +316 -0
- truthound/datasources/sql/oracle.py +427 -0
- truthound/datasources/sql/postgresql.py +321 -0
- truthound/datasources/sql/redshift.py +479 -0
- truthound/datasources/sql/snowflake.py +439 -0
- truthound/datasources/sql/sqlite.py +286 -0
- truthound/datasources/sql/sqlserver.py +437 -0
- truthound/datasources/streaming/__init__.py +47 -0
- truthound/datasources/streaming/base.py +350 -0
- truthound/datasources/streaming/kafka.py +670 -0
- truthound/decorators.py +98 -0
- truthound/docs/__init__.py +69 -0
- truthound/docs/extractor.py +971 -0
- truthound/docs/generator.py +601 -0
- truthound/docs/parser.py +1037 -0
- truthound/docs/renderer.py +999 -0
- truthound/drift/__init__.py +22 -0
- truthound/drift/compare.py +189 -0
- truthound/drift/detectors.py +464 -0
- truthound/drift/report.py +160 -0
- truthound/execution/__init__.py +65 -0
- truthound/execution/_protocols.py +324 -0
- truthound/execution/base.py +576 -0
- truthound/execution/distributed/__init__.py +179 -0
- truthound/execution/distributed/aggregations.py +731 -0
- truthound/execution/distributed/arrow_bridge.py +817 -0
- truthound/execution/distributed/base.py +550 -0
- truthound/execution/distributed/dask_engine.py +976 -0
- truthound/execution/distributed/mixins.py +766 -0
- truthound/execution/distributed/protocols.py +756 -0
- truthound/execution/distributed/ray_engine.py +1127 -0
- truthound/execution/distributed/registry.py +446 -0
- truthound/execution/distributed/spark_engine.py +1011 -0
- truthound/execution/distributed/validator_adapter.py +682 -0
- truthound/execution/pandas_engine.py +401 -0
- truthound/execution/polars_engine.py +497 -0
- truthound/execution/pushdown/__init__.py +230 -0
- truthound/execution/pushdown/ast.py +1550 -0
- truthound/execution/pushdown/builder.py +1550 -0
- truthound/execution/pushdown/dialects.py +1072 -0
- truthound/execution/pushdown/executor.py +829 -0
- truthound/execution/pushdown/optimizer.py +1041 -0
- truthound/execution/sql_engine.py +518 -0
- truthound/infrastructure/__init__.py +189 -0
- truthound/infrastructure/audit.py +1515 -0
- truthound/infrastructure/config.py +1133 -0
- truthound/infrastructure/encryption.py +1132 -0
- truthound/infrastructure/logging.py +1503 -0
- truthound/infrastructure/metrics.py +1220 -0
- truthound/lineage/__init__.py +89 -0
- truthound/lineage/base.py +746 -0
- truthound/lineage/impact_analysis.py +474 -0
- truthound/lineage/integrations/__init__.py +22 -0
- truthound/lineage/integrations/openlineage.py +548 -0
- truthound/lineage/tracker.py +512 -0
- truthound/lineage/visualization/__init__.py +33 -0
- truthound/lineage/visualization/protocols.py +145 -0
- truthound/lineage/visualization/renderers/__init__.py +20 -0
- truthound/lineage/visualization/renderers/cytoscape.py +329 -0
- truthound/lineage/visualization/renderers/d3.py +331 -0
- truthound/lineage/visualization/renderers/graphviz.py +276 -0
- truthound/lineage/visualization/renderers/mermaid.py +308 -0
- truthound/maskers.py +113 -0
- truthound/ml/__init__.py +124 -0
- truthound/ml/anomaly_models/__init__.py +31 -0
- truthound/ml/anomaly_models/ensemble.py +362 -0
- truthound/ml/anomaly_models/isolation_forest.py +444 -0
- truthound/ml/anomaly_models/statistical.py +392 -0
- truthound/ml/base.py +1178 -0
- truthound/ml/drift_detection/__init__.py +26 -0
- truthound/ml/drift_detection/concept.py +381 -0
- truthound/ml/drift_detection/distribution.py +361 -0
- truthound/ml/drift_detection/feature.py +442 -0
- truthound/ml/drift_detection/multivariate.py +495 -0
- truthound/ml/monitoring/__init__.py +88 -0
- truthound/ml/monitoring/alerting/__init__.py +33 -0
- truthound/ml/monitoring/alerting/handlers.py +427 -0
- truthound/ml/monitoring/alerting/rules.py +508 -0
- truthound/ml/monitoring/collectors/__init__.py +19 -0
- truthound/ml/monitoring/collectors/composite.py +105 -0
- truthound/ml/monitoring/collectors/drift.py +324 -0
- truthound/ml/monitoring/collectors/performance.py +179 -0
- truthound/ml/monitoring/collectors/quality.py +369 -0
- truthound/ml/monitoring/monitor.py +536 -0
- truthound/ml/monitoring/protocols.py +451 -0
- truthound/ml/monitoring/stores/__init__.py +15 -0
- truthound/ml/monitoring/stores/memory.py +201 -0
- truthound/ml/monitoring/stores/prometheus.py +296 -0
- truthound/ml/rule_learning/__init__.py +25 -0
- truthound/ml/rule_learning/constraint_miner.py +443 -0
- truthound/ml/rule_learning/pattern_learner.py +499 -0
- truthound/ml/rule_learning/profile_learner.py +462 -0
- truthound/multitenancy/__init__.py +326 -0
- truthound/multitenancy/core.py +852 -0
- truthound/multitenancy/integration.py +597 -0
- truthound/multitenancy/isolation.py +630 -0
- truthound/multitenancy/manager.py +770 -0
- truthound/multitenancy/middleware.py +765 -0
- truthound/multitenancy/quota.py +537 -0
- truthound/multitenancy/resolvers.py +603 -0
- truthound/multitenancy/storage.py +703 -0
- truthound/observability/__init__.py +307 -0
- truthound/observability/context.py +531 -0
- truthound/observability/instrumentation.py +611 -0
- truthound/observability/logging.py +887 -0
- truthound/observability/metrics.py +1157 -0
- truthound/observability/tracing/__init__.py +178 -0
- truthound/observability/tracing/baggage.py +310 -0
- truthound/observability/tracing/config.py +426 -0
- truthound/observability/tracing/exporter.py +787 -0
- truthound/observability/tracing/integration.py +1018 -0
- truthound/observability/tracing/otel/__init__.py +146 -0
- truthound/observability/tracing/otel/adapter.py +982 -0
- truthound/observability/tracing/otel/bridge.py +1177 -0
- truthound/observability/tracing/otel/compat.py +681 -0
- truthound/observability/tracing/otel/config.py +691 -0
- truthound/observability/tracing/otel/detection.py +327 -0
- truthound/observability/tracing/otel/protocols.py +426 -0
- truthound/observability/tracing/processor.py +561 -0
- truthound/observability/tracing/propagator.py +757 -0
- truthound/observability/tracing/provider.py +569 -0
- truthound/observability/tracing/resource.py +515 -0
- truthound/observability/tracing/sampler.py +487 -0
- truthound/observability/tracing/span.py +676 -0
- truthound/plugins/__init__.py +198 -0
- truthound/plugins/base.py +599 -0
- truthound/plugins/cli.py +680 -0
- truthound/plugins/dependencies/__init__.py +42 -0
- truthound/plugins/dependencies/graph.py +422 -0
- truthound/plugins/dependencies/resolver.py +417 -0
- truthound/plugins/discovery.py +379 -0
- truthound/plugins/docs/__init__.py +46 -0
- truthound/plugins/docs/extractor.py +444 -0
- truthound/plugins/docs/renderer.py +499 -0
- truthound/plugins/enterprise_manager.py +877 -0
- truthound/plugins/examples/__init__.py +19 -0
- truthound/plugins/examples/custom_validators.py +317 -0
- truthound/plugins/examples/slack_notifier.py +312 -0
- truthound/plugins/examples/xml_reporter.py +254 -0
- truthound/plugins/hooks.py +558 -0
- truthound/plugins/lifecycle/__init__.py +43 -0
- truthound/plugins/lifecycle/hot_reload.py +402 -0
- truthound/plugins/lifecycle/manager.py +371 -0
- truthound/plugins/manager.py +736 -0
- truthound/plugins/registry.py +338 -0
- truthound/plugins/security/__init__.py +93 -0
- truthound/plugins/security/exceptions.py +332 -0
- truthound/plugins/security/policies.py +348 -0
- truthound/plugins/security/protocols.py +643 -0
- truthound/plugins/security/sandbox/__init__.py +45 -0
- truthound/plugins/security/sandbox/context.py +158 -0
- truthound/plugins/security/sandbox/engines/__init__.py +19 -0
- truthound/plugins/security/sandbox/engines/container.py +379 -0
- truthound/plugins/security/sandbox/engines/noop.py +144 -0
- truthound/plugins/security/sandbox/engines/process.py +336 -0
- truthound/plugins/security/sandbox/factory.py +211 -0
- truthound/plugins/security/signing/__init__.py +57 -0
- truthound/plugins/security/signing/service.py +330 -0
- truthound/plugins/security/signing/trust_store.py +368 -0
- truthound/plugins/security/signing/verifier.py +459 -0
- truthound/plugins/versioning/__init__.py +41 -0
- truthound/plugins/versioning/constraints.py +297 -0
- truthound/plugins/versioning/resolver.py +329 -0
- truthound/profiler/__init__.py +1729 -0
- truthound/profiler/_lazy.py +452 -0
- truthound/profiler/ab_testing/__init__.py +80 -0
- truthound/profiler/ab_testing/analysis.py +449 -0
- truthound/profiler/ab_testing/base.py +257 -0
- truthound/profiler/ab_testing/experiment.py +395 -0
- truthound/profiler/ab_testing/tracking.py +368 -0
- truthound/profiler/auto_threshold.py +1170 -0
- truthound/profiler/base.py +579 -0
- truthound/profiler/cache_patterns.py +911 -0
- truthound/profiler/caching.py +1303 -0
- truthound/profiler/column_profiler.py +712 -0
- truthound/profiler/comparison.py +1007 -0
- truthound/profiler/custom_patterns.py +1170 -0
- truthound/profiler/dashboard/__init__.py +50 -0
- truthound/profiler/dashboard/app.py +476 -0
- truthound/profiler/dashboard/components.py +457 -0
- truthound/profiler/dashboard/config.py +72 -0
- truthound/profiler/distributed/__init__.py +83 -0
- truthound/profiler/distributed/base.py +281 -0
- truthound/profiler/distributed/dask_backend.py +498 -0
- truthound/profiler/distributed/local_backend.py +293 -0
- truthound/profiler/distributed/profiler.py +304 -0
- truthound/profiler/distributed/ray_backend.py +374 -0
- truthound/profiler/distributed/spark_backend.py +375 -0
- truthound/profiler/distributed.py +1366 -0
- truthound/profiler/enterprise_sampling.py +1065 -0
- truthound/profiler/errors.py +488 -0
- truthound/profiler/evolution/__init__.py +91 -0
- truthound/profiler/evolution/alerts.py +426 -0
- truthound/profiler/evolution/changes.py +206 -0
- truthound/profiler/evolution/compatibility.py +365 -0
- truthound/profiler/evolution/detector.py +372 -0
- truthound/profiler/evolution/protocols.py +121 -0
- truthound/profiler/generators/__init__.py +48 -0
- truthound/profiler/generators/base.py +384 -0
- truthound/profiler/generators/ml_rules.py +375 -0
- truthound/profiler/generators/pattern_rules.py +384 -0
- truthound/profiler/generators/schema_rules.py +267 -0
- truthound/profiler/generators/stats_rules.py +324 -0
- truthound/profiler/generators/suite_generator.py +857 -0
- truthound/profiler/i18n.py +1542 -0
- truthound/profiler/incremental.py +554 -0
- truthound/profiler/incremental_validation.py +1710 -0
- truthound/profiler/integration/__init__.py +73 -0
- truthound/profiler/integration/adapters.py +345 -0
- truthound/profiler/integration/context.py +371 -0
- truthound/profiler/integration/executor.py +527 -0
- truthound/profiler/integration/naming.py +75 -0
- truthound/profiler/integration/protocols.py +243 -0
- truthound/profiler/memory.py +1185 -0
- truthound/profiler/migration/__init__.py +60 -0
- truthound/profiler/migration/base.py +345 -0
- truthound/profiler/migration/manager.py +444 -0
- truthound/profiler/migration/v1_0_to_v1_1.py +484 -0
- truthound/profiler/ml/__init__.py +73 -0
- truthound/profiler/ml/base.py +244 -0
- truthound/profiler/ml/classifier.py +507 -0
- truthound/profiler/ml/feature_extraction.py +604 -0
- truthound/profiler/ml/pretrained.py +448 -0
- truthound/profiler/ml_inference.py +1276 -0
- truthound/profiler/native_patterns.py +815 -0
- truthound/profiler/observability.py +1184 -0
- truthound/profiler/process_timeout.py +1566 -0
- truthound/profiler/progress.py +568 -0
- truthound/profiler/progress_callbacks.py +1734 -0
- truthound/profiler/quality.py +1345 -0
- truthound/profiler/resilience.py +1180 -0
- truthound/profiler/sampled_matcher.py +794 -0
- truthound/profiler/sampling.py +1288 -0
- truthound/profiler/scheduling/__init__.py +82 -0
- truthound/profiler/scheduling/protocols.py +214 -0
- truthound/profiler/scheduling/scheduler.py +474 -0
- truthound/profiler/scheduling/storage.py +457 -0
- truthound/profiler/scheduling/triggers.py +449 -0
- truthound/profiler/schema.py +603 -0
- truthound/profiler/streaming.py +685 -0
- truthound/profiler/streaming_patterns.py +1354 -0
- truthound/profiler/suite_cli.py +625 -0
- truthound/profiler/suite_config.py +789 -0
- truthound/profiler/suite_export.py +1268 -0
- truthound/profiler/table_profiler.py +547 -0
- truthound/profiler/timeout.py +565 -0
- truthound/profiler/validation.py +1532 -0
- truthound/profiler/visualization/__init__.py +118 -0
- truthound/profiler/visualization/base.py +346 -0
- truthound/profiler/visualization/generator.py +1259 -0
- truthound/profiler/visualization/plotly_renderer.py +811 -0
- truthound/profiler/visualization/renderers.py +669 -0
- truthound/profiler/visualization/sections.py +540 -0
- truthound/profiler/visualization.py +2122 -0
- truthound/profiler/yaml_validation.py +1151 -0
- truthound/py.typed +0 -0
- truthound/ratelimit/__init__.py +248 -0
- truthound/ratelimit/algorithms.py +1108 -0
- truthound/ratelimit/core.py +573 -0
- truthound/ratelimit/integration.py +532 -0
- truthound/ratelimit/limiter.py +663 -0
- truthound/ratelimit/middleware.py +700 -0
- truthound/ratelimit/policy.py +792 -0
- truthound/ratelimit/storage.py +763 -0
- truthound/rbac/__init__.py +340 -0
- truthound/rbac/core.py +976 -0
- truthound/rbac/integration.py +760 -0
- truthound/rbac/manager.py +1052 -0
- truthound/rbac/middleware.py +842 -0
- truthound/rbac/policy.py +954 -0
- truthound/rbac/storage.py +878 -0
- truthound/realtime/__init__.py +141 -0
- truthound/realtime/adapters/__init__.py +43 -0
- truthound/realtime/adapters/base.py +533 -0
- truthound/realtime/adapters/kafka.py +487 -0
- truthound/realtime/adapters/kinesis.py +479 -0
- truthound/realtime/adapters/mock.py +243 -0
- truthound/realtime/base.py +553 -0
- truthound/realtime/factory.py +382 -0
- truthound/realtime/incremental.py +660 -0
- truthound/realtime/processing/__init__.py +67 -0
- truthound/realtime/processing/exactly_once.py +575 -0
- truthound/realtime/processing/state.py +547 -0
- truthound/realtime/processing/windows.py +647 -0
- truthound/realtime/protocols.py +569 -0
- truthound/realtime/streaming.py +605 -0
- truthound/realtime/testing/__init__.py +32 -0
- truthound/realtime/testing/containers.py +615 -0
- truthound/realtime/testing/fixtures.py +484 -0
- truthound/report.py +280 -0
- truthound/reporters/__init__.py +46 -0
- truthound/reporters/_protocols.py +30 -0
- truthound/reporters/base.py +324 -0
- truthound/reporters/ci/__init__.py +66 -0
- truthound/reporters/ci/azure.py +436 -0
- truthound/reporters/ci/base.py +509 -0
- truthound/reporters/ci/bitbucket.py +567 -0
- truthound/reporters/ci/circleci.py +547 -0
- truthound/reporters/ci/detection.py +364 -0
- truthound/reporters/ci/factory.py +182 -0
- truthound/reporters/ci/github.py +388 -0
- truthound/reporters/ci/gitlab.py +471 -0
- truthound/reporters/ci/jenkins.py +525 -0
- truthound/reporters/console_reporter.py +299 -0
- truthound/reporters/factory.py +211 -0
- truthound/reporters/html_reporter.py +524 -0
- truthound/reporters/json_reporter.py +256 -0
- truthound/reporters/markdown_reporter.py +280 -0
- truthound/reporters/sdk/__init__.py +174 -0
- truthound/reporters/sdk/builder.py +558 -0
- truthound/reporters/sdk/mixins.py +1150 -0
- truthound/reporters/sdk/schema.py +1493 -0
- truthound/reporters/sdk/templates.py +666 -0
- truthound/reporters/sdk/testing.py +968 -0
- truthound/scanners.py +170 -0
- truthound/scheduling/__init__.py +122 -0
- truthound/scheduling/cron.py +1136 -0
- truthound/scheduling/presets.py +212 -0
- truthound/schema.py +275 -0
- truthound/secrets/__init__.py +173 -0
- truthound/secrets/base.py +618 -0
- truthound/secrets/cloud.py +682 -0
- truthound/secrets/integration.py +507 -0
- truthound/secrets/manager.py +633 -0
- truthound/secrets/oidc/__init__.py +172 -0
- truthound/secrets/oidc/base.py +902 -0
- truthound/secrets/oidc/credential_provider.py +623 -0
- truthound/secrets/oidc/exchangers.py +1001 -0
- truthound/secrets/oidc/github/__init__.py +110 -0
- truthound/secrets/oidc/github/claims.py +718 -0
- truthound/secrets/oidc/github/enhanced_provider.py +693 -0
- truthound/secrets/oidc/github/trust_policy.py +742 -0
- truthound/secrets/oidc/github/verification.py +723 -0
- truthound/secrets/oidc/github/workflow.py +691 -0
- truthound/secrets/oidc/providers.py +825 -0
- truthound/secrets/providers.py +506 -0
- truthound/secrets/resolver.py +495 -0
- truthound/stores/__init__.py +177 -0
- truthound/stores/backends/__init__.py +18 -0
- truthound/stores/backends/_protocols.py +340 -0
- truthound/stores/backends/azure_blob.py +530 -0
- truthound/stores/backends/concurrent_filesystem.py +915 -0
- truthound/stores/backends/connection_pool.py +1365 -0
- truthound/stores/backends/database.py +743 -0
- truthound/stores/backends/filesystem.py +538 -0
- truthound/stores/backends/gcs.py +399 -0
- truthound/stores/backends/memory.py +354 -0
- truthound/stores/backends/s3.py +434 -0
- truthound/stores/backpressure/__init__.py +84 -0
- truthound/stores/backpressure/base.py +375 -0
- truthound/stores/backpressure/circuit_breaker.py +434 -0
- truthound/stores/backpressure/monitor.py +376 -0
- truthound/stores/backpressure/strategies.py +677 -0
- truthound/stores/base.py +551 -0
- truthound/stores/batching/__init__.py +65 -0
- truthound/stores/batching/base.py +305 -0
- truthound/stores/batching/buffer.py +370 -0
- truthound/stores/batching/store.py +248 -0
- truthound/stores/batching/writer.py +521 -0
- truthound/stores/caching/__init__.py +60 -0
- truthound/stores/caching/backends.py +684 -0
- truthound/stores/caching/base.py +356 -0
- truthound/stores/caching/store.py +305 -0
- truthound/stores/compression/__init__.py +193 -0
- truthound/stores/compression/adaptive.py +694 -0
- truthound/stores/compression/base.py +514 -0
- truthound/stores/compression/pipeline.py +868 -0
- truthound/stores/compression/providers.py +672 -0
- truthound/stores/compression/streaming.py +832 -0
- truthound/stores/concurrency/__init__.py +81 -0
- truthound/stores/concurrency/atomic.py +556 -0
- truthound/stores/concurrency/index.py +775 -0
- truthound/stores/concurrency/locks.py +576 -0
- truthound/stores/concurrency/manager.py +482 -0
- truthound/stores/encryption/__init__.py +297 -0
- truthound/stores/encryption/base.py +952 -0
- truthound/stores/encryption/keys.py +1191 -0
- truthound/stores/encryption/pipeline.py +903 -0
- truthound/stores/encryption/providers.py +953 -0
- truthound/stores/encryption/streaming.py +950 -0
- truthound/stores/expectations.py +227 -0
- truthound/stores/factory.py +246 -0
- truthound/stores/migration/__init__.py +75 -0
- truthound/stores/migration/base.py +480 -0
- truthound/stores/migration/manager.py +347 -0
- truthound/stores/migration/registry.py +382 -0
- truthound/stores/migration/store.py +559 -0
- truthound/stores/observability/__init__.py +106 -0
- truthound/stores/observability/audit.py +718 -0
- truthound/stores/observability/config.py +270 -0
- truthound/stores/observability/factory.py +208 -0
- truthound/stores/observability/metrics.py +636 -0
- truthound/stores/observability/protocols.py +410 -0
- truthound/stores/observability/store.py +570 -0
- truthound/stores/observability/tracing.py +784 -0
- truthound/stores/replication/__init__.py +76 -0
- truthound/stores/replication/base.py +260 -0
- truthound/stores/replication/monitor.py +269 -0
- truthound/stores/replication/store.py +439 -0
- truthound/stores/replication/syncer.py +391 -0
- truthound/stores/results.py +359 -0
- truthound/stores/retention/__init__.py +77 -0
- truthound/stores/retention/base.py +378 -0
- truthound/stores/retention/policies.py +621 -0
- truthound/stores/retention/scheduler.py +279 -0
- truthound/stores/retention/store.py +526 -0
- truthound/stores/streaming/__init__.py +138 -0
- truthound/stores/streaming/base.py +801 -0
- truthound/stores/streaming/database.py +984 -0
- truthound/stores/streaming/filesystem.py +719 -0
- truthound/stores/streaming/reader.py +629 -0
- truthound/stores/streaming/s3.py +843 -0
- truthound/stores/streaming/writer.py +790 -0
- truthound/stores/tiering/__init__.py +108 -0
- truthound/stores/tiering/base.py +462 -0
- truthound/stores/tiering/manager.py +249 -0
- truthound/stores/tiering/policies.py +692 -0
- truthound/stores/tiering/store.py +526 -0
- truthound/stores/versioning/__init__.py +56 -0
- truthound/stores/versioning/base.py +376 -0
- truthound/stores/versioning/store.py +660 -0
- truthound/stores/versioning/strategies.py +353 -0
- truthound/types.py +56 -0
- truthound/validators/__init__.py +774 -0
- truthound/validators/aggregate/__init__.py +27 -0
- truthound/validators/aggregate/central.py +116 -0
- truthound/validators/aggregate/extremes.py +116 -0
- truthound/validators/aggregate/spread.py +118 -0
- truthound/validators/aggregate/sum.py +64 -0
- truthound/validators/aggregate/type.py +78 -0
- truthound/validators/anomaly/__init__.py +93 -0
- truthound/validators/anomaly/base.py +431 -0
- truthound/validators/anomaly/ml_based.py +1190 -0
- truthound/validators/anomaly/multivariate.py +647 -0
- truthound/validators/anomaly/statistical.py +599 -0
- truthound/validators/base.py +1089 -0
- truthound/validators/business_rule/__init__.py +46 -0
- truthound/validators/business_rule/base.py +147 -0
- truthound/validators/business_rule/checksum.py +509 -0
- truthound/validators/business_rule/financial.py +526 -0
- truthound/validators/cache.py +733 -0
- truthound/validators/completeness/__init__.py +39 -0
- truthound/validators/completeness/conditional.py +73 -0
- truthound/validators/completeness/default.py +98 -0
- truthound/validators/completeness/empty.py +103 -0
- truthound/validators/completeness/nan.py +337 -0
- truthound/validators/completeness/null.py +152 -0
- truthound/validators/cross_table/__init__.py +17 -0
- truthound/validators/cross_table/aggregate.py +333 -0
- truthound/validators/cross_table/row_count.py +122 -0
- truthound/validators/datetime/__init__.py +29 -0
- truthound/validators/datetime/format.py +78 -0
- truthound/validators/datetime/freshness.py +269 -0
- truthound/validators/datetime/order.py +73 -0
- truthound/validators/datetime/parseable.py +185 -0
- truthound/validators/datetime/range.py +202 -0
- truthound/validators/datetime/timezone.py +69 -0
- truthound/validators/distribution/__init__.py +49 -0
- truthound/validators/distribution/distribution.py +128 -0
- truthound/validators/distribution/monotonic.py +119 -0
- truthound/validators/distribution/outlier.py +178 -0
- truthound/validators/distribution/quantile.py +80 -0
- truthound/validators/distribution/range.py +254 -0
- truthound/validators/distribution/set.py +125 -0
- truthound/validators/distribution/statistical.py +459 -0
- truthound/validators/drift/__init__.py +79 -0
- truthound/validators/drift/base.py +427 -0
- truthound/validators/drift/multi_feature.py +401 -0
- truthound/validators/drift/numeric.py +395 -0
- truthound/validators/drift/psi.py +446 -0
- truthound/validators/drift/statistical.py +510 -0
- truthound/validators/enterprise.py +1658 -0
- truthound/validators/geospatial/__init__.py +80 -0
- truthound/validators/geospatial/base.py +97 -0
- truthound/validators/geospatial/boundary.py +238 -0
- truthound/validators/geospatial/coordinate.py +351 -0
- truthound/validators/geospatial/distance.py +399 -0
- truthound/validators/geospatial/polygon.py +665 -0
- truthound/validators/i18n/__init__.py +308 -0
- truthound/validators/i18n/bidi.py +571 -0
- truthound/validators/i18n/catalogs.py +570 -0
- truthound/validators/i18n/dialects.py +763 -0
- truthound/validators/i18n/extended_catalogs.py +549 -0
- truthound/validators/i18n/formatting.py +1434 -0
- truthound/validators/i18n/loader.py +1020 -0
- truthound/validators/i18n/messages.py +521 -0
- truthound/validators/i18n/plural.py +683 -0
- truthound/validators/i18n/protocols.py +855 -0
- truthound/validators/i18n/tms.py +1162 -0
- truthound/validators/localization/__init__.py +53 -0
- truthound/validators/localization/base.py +122 -0
- truthound/validators/localization/chinese.py +362 -0
- truthound/validators/localization/japanese.py +275 -0
- truthound/validators/localization/korean.py +524 -0
- truthound/validators/memory/__init__.py +94 -0
- truthound/validators/memory/approximate_knn.py +506 -0
- truthound/validators/memory/base.py +547 -0
- truthound/validators/memory/sgd_online.py +719 -0
- truthound/validators/memory/streaming_ecdf.py +753 -0
- truthound/validators/ml_feature/__init__.py +54 -0
- truthound/validators/ml_feature/base.py +249 -0
- truthound/validators/ml_feature/correlation.py +299 -0
- truthound/validators/ml_feature/leakage.py +344 -0
- truthound/validators/ml_feature/null_impact.py +270 -0
- truthound/validators/ml_feature/scale.py +264 -0
- truthound/validators/multi_column/__init__.py +89 -0
- truthound/validators/multi_column/arithmetic.py +284 -0
- truthound/validators/multi_column/base.py +231 -0
- truthound/validators/multi_column/comparison.py +273 -0
- truthound/validators/multi_column/consistency.py +312 -0
- truthound/validators/multi_column/statistical.py +299 -0
- truthound/validators/optimization/__init__.py +164 -0
- truthound/validators/optimization/aggregation.py +563 -0
- truthound/validators/optimization/covariance.py +556 -0
- truthound/validators/optimization/geo.py +626 -0
- truthound/validators/optimization/graph.py +587 -0
- truthound/validators/optimization/orchestrator.py +970 -0
- truthound/validators/optimization/profiling.py +1312 -0
- truthound/validators/privacy/__init__.py +223 -0
- truthound/validators/privacy/base.py +635 -0
- truthound/validators/privacy/ccpa.py +670 -0
- truthound/validators/privacy/gdpr.py +728 -0
- truthound/validators/privacy/global_patterns.py +604 -0
- truthound/validators/privacy/plugins.py +867 -0
- truthound/validators/profiling/__init__.py +52 -0
- truthound/validators/profiling/base.py +175 -0
- truthound/validators/profiling/cardinality.py +312 -0
- truthound/validators/profiling/entropy.py +391 -0
- truthound/validators/profiling/frequency.py +455 -0
- truthound/validators/pushdown_support.py +660 -0
- truthound/validators/query/__init__.py +91 -0
- truthound/validators/query/aggregate.py +346 -0
- truthound/validators/query/base.py +246 -0
- truthound/validators/query/column.py +249 -0
- truthound/validators/query/expression.py +274 -0
- truthound/validators/query/result.py +323 -0
- truthound/validators/query/row_count.py +264 -0
- truthound/validators/referential/__init__.py +80 -0
- truthound/validators/referential/base.py +395 -0
- truthound/validators/referential/cascade.py +391 -0
- truthound/validators/referential/circular.py +563 -0
- truthound/validators/referential/foreign_key.py +624 -0
- truthound/validators/referential/orphan.py +485 -0
- truthound/validators/registry.py +112 -0
- truthound/validators/schema/__init__.py +41 -0
- truthound/validators/schema/column_count.py +142 -0
- truthound/validators/schema/column_exists.py +80 -0
- truthound/validators/schema/column_order.py +82 -0
- truthound/validators/schema/column_pair.py +85 -0
- truthound/validators/schema/column_pair_set.py +195 -0
- truthound/validators/schema/column_type.py +94 -0
- truthound/validators/schema/multi_column.py +53 -0
- truthound/validators/schema/multi_column_aggregate.py +175 -0
- truthound/validators/schema/referential.py +274 -0
- truthound/validators/schema/table_schema.py +91 -0
- truthound/validators/schema_validator.py +219 -0
- truthound/validators/sdk/__init__.py +250 -0
- truthound/validators/sdk/builder.py +680 -0
- truthound/validators/sdk/decorators.py +474 -0
- truthound/validators/sdk/enterprise/__init__.py +211 -0
- truthound/validators/sdk/enterprise/docs.py +725 -0
- truthound/validators/sdk/enterprise/fuzzing.py +659 -0
- truthound/validators/sdk/enterprise/licensing.py +709 -0
- truthound/validators/sdk/enterprise/manager.py +543 -0
- truthound/validators/sdk/enterprise/resources.py +628 -0
- truthound/validators/sdk/enterprise/sandbox.py +766 -0
- truthound/validators/sdk/enterprise/signing.py +603 -0
- truthound/validators/sdk/enterprise/templates.py +865 -0
- truthound/validators/sdk/enterprise/versioning.py +659 -0
- truthound/validators/sdk/templates.py +757 -0
- truthound/validators/sdk/testing.py +807 -0
- truthound/validators/security/__init__.py +181 -0
- truthound/validators/security/redos/__init__.py +182 -0
- truthound/validators/security/redos/core.py +861 -0
- truthound/validators/security/redos/cpu_monitor.py +593 -0
- truthound/validators/security/redos/cve_database.py +791 -0
- truthound/validators/security/redos/ml/__init__.py +155 -0
- truthound/validators/security/redos/ml/base.py +785 -0
- truthound/validators/security/redos/ml/datasets.py +618 -0
- truthound/validators/security/redos/ml/features.py +359 -0
- truthound/validators/security/redos/ml/models.py +1000 -0
- truthound/validators/security/redos/ml/predictor.py +507 -0
- truthound/validators/security/redos/ml/storage.py +632 -0
- truthound/validators/security/redos/ml/training.py +571 -0
- truthound/validators/security/redos/ml_analyzer.py +937 -0
- truthound/validators/security/redos/optimizer.py +674 -0
- truthound/validators/security/redos/profiler.py +682 -0
- truthound/validators/security/redos/re2_engine.py +709 -0
- truthound/validators/security/redos.py +886 -0
- truthound/validators/security/sql_security.py +1247 -0
- truthound/validators/streaming/__init__.py +126 -0
- truthound/validators/streaming/base.py +292 -0
- truthound/validators/streaming/completeness.py +210 -0
- truthound/validators/streaming/mixin.py +575 -0
- truthound/validators/streaming/range.py +308 -0
- truthound/validators/streaming/sources.py +846 -0
- truthound/validators/string/__init__.py +57 -0
- truthound/validators/string/casing.py +158 -0
- truthound/validators/string/charset.py +96 -0
- truthound/validators/string/format.py +501 -0
- truthound/validators/string/json.py +77 -0
- truthound/validators/string/json_schema.py +184 -0
- truthound/validators/string/length.py +104 -0
- truthound/validators/string/like_pattern.py +237 -0
- truthound/validators/string/regex.py +202 -0
- truthound/validators/string/regex_extended.py +435 -0
- truthound/validators/table/__init__.py +88 -0
- truthound/validators/table/base.py +78 -0
- truthound/validators/table/column_count.py +198 -0
- truthound/validators/table/freshness.py +362 -0
- truthound/validators/table/row_count.py +251 -0
- truthound/validators/table/schema.py +333 -0
- truthound/validators/table/size.py +285 -0
- truthound/validators/timeout/__init__.py +102 -0
- truthound/validators/timeout/advanced/__init__.py +247 -0
- truthound/validators/timeout/advanced/circuit_breaker.py +675 -0
- truthound/validators/timeout/advanced/prediction.py +773 -0
- truthound/validators/timeout/advanced/priority.py +618 -0
- truthound/validators/timeout/advanced/redis_backend.py +770 -0
- truthound/validators/timeout/advanced/retry.py +721 -0
- truthound/validators/timeout/advanced/sampling.py +788 -0
- truthound/validators/timeout/advanced/sla.py +661 -0
- truthound/validators/timeout/advanced/telemetry.py +804 -0
- truthound/validators/timeout/cascade.py +477 -0
- truthound/validators/timeout/deadline.py +657 -0
- truthound/validators/timeout/degradation.py +525 -0
- truthound/validators/timeout/distributed.py +597 -0
- truthound/validators/timeseries/__init__.py +89 -0
- truthound/validators/timeseries/base.py +326 -0
- truthound/validators/timeseries/completeness.py +617 -0
- truthound/validators/timeseries/gap.py +485 -0
- truthound/validators/timeseries/monotonic.py +310 -0
- truthound/validators/timeseries/seasonality.py +422 -0
- truthound/validators/timeseries/trend.py +510 -0
- truthound/validators/uniqueness/__init__.py +59 -0
- truthound/validators/uniqueness/approximate.py +475 -0
- truthound/validators/uniqueness/distinct_values.py +253 -0
- truthound/validators/uniqueness/duplicate.py +118 -0
- truthound/validators/uniqueness/primary_key.py +140 -0
- truthound/validators/uniqueness/unique.py +191 -0
- truthound/validators/uniqueness/within_record.py +599 -0
- truthound/validators/utils.py +756 -0
- truthound-1.0.8.dist-info/METADATA +474 -0
- truthound-1.0.8.dist-info/RECORD +877 -0
- truthound-1.0.8.dist-info/WHEEL +4 -0
- truthound-1.0.8.dist-info/entry_points.txt +2 -0
- 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
|