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,1184 @@
|
|
|
1
|
+
"""Observability module with OpenTelemetry integration for profiler metrics.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive observability for the profiling system:
|
|
4
|
+
- OpenTelemetry tracing for operation tracking
|
|
5
|
+
- Metrics collection for performance monitoring
|
|
6
|
+
- Structured logging integration
|
|
7
|
+
- Custom metric exporters
|
|
8
|
+
|
|
9
|
+
Key features:
|
|
10
|
+
- Pluggable exporter architecture
|
|
11
|
+
- Automatic span creation for profiling operations
|
|
12
|
+
- Histogram and counter metrics
|
|
13
|
+
- Context propagation
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
from truthound.profiler.observability import (
|
|
17
|
+
ProfilerTelemetry,
|
|
18
|
+
MetricsCollector,
|
|
19
|
+
traced,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Initialize telemetry
|
|
23
|
+
telemetry = ProfilerTelemetry(service_name="truthound-profiler")
|
|
24
|
+
|
|
25
|
+
# Use decorator for automatic tracing
|
|
26
|
+
@traced("profile_column")
|
|
27
|
+
def profile_column(col: str) -> ColumnProfile:
|
|
28
|
+
# profiling logic...
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
# Or manual tracing
|
|
32
|
+
with telemetry.span("custom_operation") as span:
|
|
33
|
+
span.set_attribute("column_count", 10)
|
|
34
|
+
# operation...
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import logging
|
|
40
|
+
import threading
|
|
41
|
+
import time
|
|
42
|
+
from abc import ABC, abstractmethod
|
|
43
|
+
from collections import defaultdict
|
|
44
|
+
from contextlib import contextmanager
|
|
45
|
+
from dataclasses import dataclass, field
|
|
46
|
+
from datetime import datetime, timedelta
|
|
47
|
+
from enum import Enum
|
|
48
|
+
from functools import wraps
|
|
49
|
+
from typing import (
|
|
50
|
+
TYPE_CHECKING,
|
|
51
|
+
Any,
|
|
52
|
+
Callable,
|
|
53
|
+
ContextManager,
|
|
54
|
+
Generator,
|
|
55
|
+
Generic,
|
|
56
|
+
Protocol,
|
|
57
|
+
TypeVar,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# =============================================================================
|
|
61
|
+
# Types
|
|
62
|
+
# =============================================================================
|
|
63
|
+
|
|
64
|
+
T = TypeVar("T")
|
|
65
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class MetricType(str, Enum):
|
|
69
|
+
"""Types of metrics."""
|
|
70
|
+
|
|
71
|
+
COUNTER = "counter"
|
|
72
|
+
GAUGE = "gauge"
|
|
73
|
+
HISTOGRAM = "histogram"
|
|
74
|
+
SUMMARY = "summary"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class SpanStatus(str, Enum):
|
|
78
|
+
"""Span status codes."""
|
|
79
|
+
|
|
80
|
+
OK = "ok"
|
|
81
|
+
ERROR = "error"
|
|
82
|
+
UNSET = "unset"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# =============================================================================
|
|
86
|
+
# Span Protocol
|
|
87
|
+
# =============================================================================
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class SpanProtocol(Protocol):
|
|
91
|
+
"""Protocol for span interface (compatible with OpenTelemetry)."""
|
|
92
|
+
|
|
93
|
+
def set_attribute(self, key: str, value: Any) -> None:
|
|
94
|
+
"""Set span attribute."""
|
|
95
|
+
...
|
|
96
|
+
|
|
97
|
+
def set_status(self, status: SpanStatus, description: str = "") -> None:
|
|
98
|
+
"""Set span status."""
|
|
99
|
+
...
|
|
100
|
+
|
|
101
|
+
def add_event(self, name: str, attributes: dict[str, Any] | None = None) -> None:
|
|
102
|
+
"""Add event to span."""
|
|
103
|
+
...
|
|
104
|
+
|
|
105
|
+
def record_exception(self, exception: BaseException) -> None:
|
|
106
|
+
"""Record exception in span."""
|
|
107
|
+
...
|
|
108
|
+
|
|
109
|
+
def end(self) -> None:
|
|
110
|
+
"""End the span."""
|
|
111
|
+
...
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# =============================================================================
|
|
115
|
+
# Span Implementation
|
|
116
|
+
# =============================================================================
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class SpanEvent:
|
|
121
|
+
"""Event recorded in a span."""
|
|
122
|
+
|
|
123
|
+
name: str
|
|
124
|
+
timestamp: datetime
|
|
125
|
+
attributes: dict[str, Any] = field(default_factory=dict)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class Span:
|
|
130
|
+
"""Lightweight span implementation.
|
|
131
|
+
|
|
132
|
+
Compatible with OpenTelemetry Span interface but can work standalone.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
name: str
|
|
136
|
+
trace_id: str
|
|
137
|
+
span_id: str
|
|
138
|
+
parent_id: str | None = None
|
|
139
|
+
start_time: datetime = field(default_factory=datetime.now)
|
|
140
|
+
end_time: datetime | None = None
|
|
141
|
+
status: SpanStatus = SpanStatus.UNSET
|
|
142
|
+
status_description: str = ""
|
|
143
|
+
attributes: dict[str, Any] = field(default_factory=dict)
|
|
144
|
+
events: list[SpanEvent] = field(default_factory=list)
|
|
145
|
+
exception: BaseException | None = None
|
|
146
|
+
|
|
147
|
+
def set_attribute(self, key: str, value: Any) -> None:
|
|
148
|
+
"""Set span attribute."""
|
|
149
|
+
self.attributes[key] = value
|
|
150
|
+
|
|
151
|
+
def set_status(self, status: SpanStatus, description: str = "") -> None:
|
|
152
|
+
"""Set span status."""
|
|
153
|
+
self.status = status
|
|
154
|
+
self.status_description = description
|
|
155
|
+
|
|
156
|
+
def add_event(self, name: str, attributes: dict[str, Any] | None = None) -> None:
|
|
157
|
+
"""Add event to span."""
|
|
158
|
+
self.events.append(
|
|
159
|
+
SpanEvent(
|
|
160
|
+
name=name,
|
|
161
|
+
timestamp=datetime.now(),
|
|
162
|
+
attributes=attributes or {},
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def record_exception(self, exception: BaseException) -> None:
|
|
167
|
+
"""Record exception in span."""
|
|
168
|
+
self.exception = exception
|
|
169
|
+
self.set_status(SpanStatus.ERROR, str(exception))
|
|
170
|
+
self.add_event(
|
|
171
|
+
"exception",
|
|
172
|
+
{
|
|
173
|
+
"exception.type": type(exception).__name__,
|
|
174
|
+
"exception.message": str(exception),
|
|
175
|
+
},
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def end(self) -> None:
|
|
179
|
+
"""End the span."""
|
|
180
|
+
self.end_time = datetime.now()
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def duration_ms(self) -> float:
|
|
184
|
+
"""Get span duration in milliseconds."""
|
|
185
|
+
if self.end_time is None:
|
|
186
|
+
return (datetime.now() - self.start_time).total_seconds() * 1000
|
|
187
|
+
return (self.end_time - self.start_time).total_seconds() * 1000
|
|
188
|
+
|
|
189
|
+
def to_dict(self) -> dict[str, Any]:
|
|
190
|
+
"""Convert to dictionary."""
|
|
191
|
+
return {
|
|
192
|
+
"name": self.name,
|
|
193
|
+
"trace_id": self.trace_id,
|
|
194
|
+
"span_id": self.span_id,
|
|
195
|
+
"parent_id": self.parent_id,
|
|
196
|
+
"start_time": self.start_time.isoformat(),
|
|
197
|
+
"end_time": self.end_time.isoformat() if self.end_time else None,
|
|
198
|
+
"duration_ms": self.duration_ms,
|
|
199
|
+
"status": self.status.value,
|
|
200
|
+
"status_description": self.status_description,
|
|
201
|
+
"attributes": self.attributes,
|
|
202
|
+
"events": [
|
|
203
|
+
{
|
|
204
|
+
"name": e.name,
|
|
205
|
+
"timestamp": e.timestamp.isoformat(),
|
|
206
|
+
"attributes": e.attributes,
|
|
207
|
+
}
|
|
208
|
+
for e in self.events
|
|
209
|
+
],
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# =============================================================================
|
|
214
|
+
# Metrics
|
|
215
|
+
# =============================================================================
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@dataclass
|
|
219
|
+
class MetricValue:
|
|
220
|
+
"""Single metric measurement."""
|
|
221
|
+
|
|
222
|
+
value: float
|
|
223
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
224
|
+
labels: dict[str, str] = field(default_factory=dict)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class Metric(ABC):
|
|
228
|
+
"""Abstract base class for metrics."""
|
|
229
|
+
|
|
230
|
+
def __init__(
|
|
231
|
+
self,
|
|
232
|
+
name: str,
|
|
233
|
+
description: str = "",
|
|
234
|
+
unit: str = "",
|
|
235
|
+
labels: list[str] | None = None,
|
|
236
|
+
):
|
|
237
|
+
self.name = name
|
|
238
|
+
self.description = description
|
|
239
|
+
self.unit = unit
|
|
240
|
+
self.label_names = labels or []
|
|
241
|
+
self._lock = threading.Lock()
|
|
242
|
+
|
|
243
|
+
@property
|
|
244
|
+
@abstractmethod
|
|
245
|
+
def metric_type(self) -> MetricType:
|
|
246
|
+
"""Get metric type."""
|
|
247
|
+
pass
|
|
248
|
+
|
|
249
|
+
@abstractmethod
|
|
250
|
+
def collect(self) -> list[MetricValue]:
|
|
251
|
+
"""Collect current metric values."""
|
|
252
|
+
pass
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class Counter(Metric):
|
|
256
|
+
"""Monotonically increasing counter metric."""
|
|
257
|
+
|
|
258
|
+
def __init__(
|
|
259
|
+
self,
|
|
260
|
+
name: str,
|
|
261
|
+
description: str = "",
|
|
262
|
+
labels: list[str] | None = None,
|
|
263
|
+
):
|
|
264
|
+
super().__init__(name, description, labels=labels)
|
|
265
|
+
self._values: dict[tuple[str, ...], float] = defaultdict(float)
|
|
266
|
+
|
|
267
|
+
@property
|
|
268
|
+
def metric_type(self) -> MetricType:
|
|
269
|
+
return MetricType.COUNTER
|
|
270
|
+
|
|
271
|
+
def inc(self, value: float = 1.0, **labels: str) -> None:
|
|
272
|
+
"""Increment counter."""
|
|
273
|
+
key = self._label_key(labels)
|
|
274
|
+
with self._lock:
|
|
275
|
+
self._values[key] += value
|
|
276
|
+
|
|
277
|
+
def _label_key(self, labels: dict[str, str]) -> tuple[str, ...]:
|
|
278
|
+
"""Create label key tuple."""
|
|
279
|
+
return tuple(labels.get(name, "") for name in self.label_names)
|
|
280
|
+
|
|
281
|
+
def collect(self) -> list[MetricValue]:
|
|
282
|
+
"""Collect current values."""
|
|
283
|
+
with self._lock:
|
|
284
|
+
return [
|
|
285
|
+
MetricValue(
|
|
286
|
+
value=value,
|
|
287
|
+
labels=dict(zip(self.label_names, key)),
|
|
288
|
+
)
|
|
289
|
+
for key, value in self._values.items()
|
|
290
|
+
]
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class Gauge(Metric):
|
|
294
|
+
"""Gauge metric that can go up and down."""
|
|
295
|
+
|
|
296
|
+
def __init__(
|
|
297
|
+
self,
|
|
298
|
+
name: str,
|
|
299
|
+
description: str = "",
|
|
300
|
+
labels: list[str] | None = None,
|
|
301
|
+
):
|
|
302
|
+
super().__init__(name, description, labels=labels)
|
|
303
|
+
self._values: dict[tuple[str, ...], float] = {}
|
|
304
|
+
|
|
305
|
+
@property
|
|
306
|
+
def metric_type(self) -> MetricType:
|
|
307
|
+
return MetricType.GAUGE
|
|
308
|
+
|
|
309
|
+
def set(self, value: float, **labels: str) -> None:
|
|
310
|
+
"""Set gauge value."""
|
|
311
|
+
key = self._label_key(labels)
|
|
312
|
+
with self._lock:
|
|
313
|
+
self._values[key] = value
|
|
314
|
+
|
|
315
|
+
def inc(self, value: float = 1.0, **labels: str) -> None:
|
|
316
|
+
"""Increment gauge."""
|
|
317
|
+
key = self._label_key(labels)
|
|
318
|
+
with self._lock:
|
|
319
|
+
self._values[key] = self._values.get(key, 0.0) + value
|
|
320
|
+
|
|
321
|
+
def dec(self, value: float = 1.0, **labels: str) -> None:
|
|
322
|
+
"""Decrement gauge."""
|
|
323
|
+
self.inc(-value, **labels)
|
|
324
|
+
|
|
325
|
+
def _label_key(self, labels: dict[str, str]) -> tuple[str, ...]:
|
|
326
|
+
"""Create label key tuple."""
|
|
327
|
+
return tuple(labels.get(name, "") for name in self.label_names)
|
|
328
|
+
|
|
329
|
+
def collect(self) -> list[MetricValue]:
|
|
330
|
+
"""Collect current values."""
|
|
331
|
+
with self._lock:
|
|
332
|
+
return [
|
|
333
|
+
MetricValue(
|
|
334
|
+
value=value,
|
|
335
|
+
labels=dict(zip(self.label_names, key)),
|
|
336
|
+
)
|
|
337
|
+
for key, value in self._values.items()
|
|
338
|
+
]
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
class Histogram(Metric):
|
|
342
|
+
"""Histogram metric for distributions."""
|
|
343
|
+
|
|
344
|
+
DEFAULT_BUCKETS = (
|
|
345
|
+
0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
def __init__(
|
|
349
|
+
self,
|
|
350
|
+
name: str,
|
|
351
|
+
description: str = "",
|
|
352
|
+
labels: list[str] | None = None,
|
|
353
|
+
buckets: tuple[float, ...] | None = None,
|
|
354
|
+
):
|
|
355
|
+
super().__init__(name, description, labels=labels)
|
|
356
|
+
self.buckets = buckets or self.DEFAULT_BUCKETS
|
|
357
|
+
self._observations: dict[tuple[str, ...], list[float]] = defaultdict(list)
|
|
358
|
+
|
|
359
|
+
@property
|
|
360
|
+
def metric_type(self) -> MetricType:
|
|
361
|
+
return MetricType.HISTOGRAM
|
|
362
|
+
|
|
363
|
+
def observe(self, value: float, **labels: str) -> None:
|
|
364
|
+
"""Record an observation."""
|
|
365
|
+
key = self._label_key(labels)
|
|
366
|
+
with self._lock:
|
|
367
|
+
self._observations[key].append(value)
|
|
368
|
+
|
|
369
|
+
@contextmanager
|
|
370
|
+
def time(self, **labels: str) -> Generator[None, None, None]:
|
|
371
|
+
"""Context manager to time operations."""
|
|
372
|
+
start = time.perf_counter()
|
|
373
|
+
try:
|
|
374
|
+
yield
|
|
375
|
+
finally:
|
|
376
|
+
duration = time.perf_counter() - start
|
|
377
|
+
self.observe(duration, **labels)
|
|
378
|
+
|
|
379
|
+
def _label_key(self, labels: dict[str, str]) -> tuple[str, ...]:
|
|
380
|
+
"""Create label key tuple."""
|
|
381
|
+
return tuple(labels.get(name, "") for name in self.label_names)
|
|
382
|
+
|
|
383
|
+
def collect(self) -> list[MetricValue]:
|
|
384
|
+
"""Collect histogram statistics."""
|
|
385
|
+
results = []
|
|
386
|
+
with self._lock:
|
|
387
|
+
for key, observations in self._observations.items():
|
|
388
|
+
if not observations:
|
|
389
|
+
continue
|
|
390
|
+
|
|
391
|
+
labels = dict(zip(self.label_names, key))
|
|
392
|
+
|
|
393
|
+
# Count and sum
|
|
394
|
+
results.append(MetricValue(
|
|
395
|
+
value=len(observations),
|
|
396
|
+
labels={**labels, "stat": "count"},
|
|
397
|
+
))
|
|
398
|
+
results.append(MetricValue(
|
|
399
|
+
value=sum(observations),
|
|
400
|
+
labels={**labels, "stat": "sum"},
|
|
401
|
+
))
|
|
402
|
+
|
|
403
|
+
# Bucket counts
|
|
404
|
+
for bucket in self.buckets:
|
|
405
|
+
count = sum(1 for v in observations if v <= bucket)
|
|
406
|
+
results.append(MetricValue(
|
|
407
|
+
value=count,
|
|
408
|
+
labels={**labels, "le": str(bucket)},
|
|
409
|
+
))
|
|
410
|
+
|
|
411
|
+
return results
|
|
412
|
+
|
|
413
|
+
def get_percentile(self, percentile: float, **labels: str) -> float | None:
|
|
414
|
+
"""Get percentile value for a label set."""
|
|
415
|
+
key = self._label_key(labels)
|
|
416
|
+
with self._lock:
|
|
417
|
+
observations = self._observations.get(key, [])
|
|
418
|
+
if not observations:
|
|
419
|
+
return None
|
|
420
|
+
sorted_obs = sorted(observations)
|
|
421
|
+
idx = int(len(sorted_obs) * percentile / 100)
|
|
422
|
+
return sorted_obs[min(idx, len(sorted_obs) - 1)]
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
# =============================================================================
|
|
426
|
+
# Metrics Collector
|
|
427
|
+
# =============================================================================
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
class MetricsCollector:
|
|
431
|
+
"""Central collector for all profiler metrics.
|
|
432
|
+
|
|
433
|
+
Provides pre-defined metrics for common profiling operations
|
|
434
|
+
and allows custom metric registration.
|
|
435
|
+
|
|
436
|
+
Example:
|
|
437
|
+
collector = MetricsCollector()
|
|
438
|
+
|
|
439
|
+
# Record profile duration
|
|
440
|
+
with collector.profile_duration.time(column="user_id"):
|
|
441
|
+
profile_column(...)
|
|
442
|
+
|
|
443
|
+
# Increment counter
|
|
444
|
+
collector.profiles_total.inc(status="success")
|
|
445
|
+
"""
|
|
446
|
+
|
|
447
|
+
def __init__(self, prefix: str = "truthound_profiler"):
|
|
448
|
+
self.prefix = prefix
|
|
449
|
+
self._metrics: dict[str, Metric] = {}
|
|
450
|
+
self._lock = threading.Lock()
|
|
451
|
+
|
|
452
|
+
# Register default metrics
|
|
453
|
+
self._register_default_metrics()
|
|
454
|
+
|
|
455
|
+
def _register_default_metrics(self) -> None:
|
|
456
|
+
"""Register standard profiler metrics."""
|
|
457
|
+
# Counters
|
|
458
|
+
self.profiles_total = self.register_counter(
|
|
459
|
+
"profiles_total",
|
|
460
|
+
"Total number of profiles completed",
|
|
461
|
+
labels=["status", "type"],
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
self.columns_profiled = self.register_counter(
|
|
465
|
+
"columns_profiled",
|
|
466
|
+
"Total number of columns profiled",
|
|
467
|
+
labels=["data_type"],
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
self.rules_generated = self.register_counter(
|
|
471
|
+
"rules_generated",
|
|
472
|
+
"Total number of rules generated",
|
|
473
|
+
labels=["category", "generator"],
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
self.cache_operations = self.register_counter(
|
|
477
|
+
"cache_operations",
|
|
478
|
+
"Cache operations",
|
|
479
|
+
labels=["operation", "result"],
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
# Gauges
|
|
483
|
+
self.active_profiles = self.register_gauge(
|
|
484
|
+
"active_profiles",
|
|
485
|
+
"Number of profiles currently in progress",
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
self.cache_size = self.register_gauge(
|
|
489
|
+
"cache_size",
|
|
490
|
+
"Current cache size",
|
|
491
|
+
labels=["backend"],
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
# Histograms
|
|
495
|
+
self.profile_duration = self.register_histogram(
|
|
496
|
+
"profile_duration_seconds",
|
|
497
|
+
"Time spent profiling",
|
|
498
|
+
labels=["operation"],
|
|
499
|
+
buckets=(0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 10.0, 30.0, 60.0),
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
self.column_profile_duration = self.register_histogram(
|
|
503
|
+
"column_profile_duration_seconds",
|
|
504
|
+
"Time spent profiling a column",
|
|
505
|
+
labels=["column_type"],
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
self.pattern_match_duration = self.register_histogram(
|
|
509
|
+
"pattern_match_duration_seconds",
|
|
510
|
+
"Time spent matching patterns",
|
|
511
|
+
labels=["pattern_name"],
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
self.rows_processed = self.register_histogram(
|
|
515
|
+
"rows_processed",
|
|
516
|
+
"Number of rows processed per profile",
|
|
517
|
+
buckets=(100, 1000, 10000, 100000, 1000000, 10000000),
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
def register_counter(
|
|
521
|
+
self,
|
|
522
|
+
name: str,
|
|
523
|
+
description: str = "",
|
|
524
|
+
labels: list[str] | None = None,
|
|
525
|
+
) -> Counter:
|
|
526
|
+
"""Register a counter metric."""
|
|
527
|
+
full_name = f"{self.prefix}_{name}"
|
|
528
|
+
metric = Counter(full_name, description, labels)
|
|
529
|
+
with self._lock:
|
|
530
|
+
self._metrics[full_name] = metric
|
|
531
|
+
return metric
|
|
532
|
+
|
|
533
|
+
def register_gauge(
|
|
534
|
+
self,
|
|
535
|
+
name: str,
|
|
536
|
+
description: str = "",
|
|
537
|
+
labels: list[str] | None = None,
|
|
538
|
+
) -> Gauge:
|
|
539
|
+
"""Register a gauge metric."""
|
|
540
|
+
full_name = f"{self.prefix}_{name}"
|
|
541
|
+
metric = Gauge(full_name, description, labels)
|
|
542
|
+
with self._lock:
|
|
543
|
+
self._metrics[full_name] = metric
|
|
544
|
+
return metric
|
|
545
|
+
|
|
546
|
+
def register_histogram(
|
|
547
|
+
self,
|
|
548
|
+
name: str,
|
|
549
|
+
description: str = "",
|
|
550
|
+
labels: list[str] | None = None,
|
|
551
|
+
buckets: tuple[float, ...] | None = None,
|
|
552
|
+
) -> Histogram:
|
|
553
|
+
"""Register a histogram metric."""
|
|
554
|
+
full_name = f"{self.prefix}_{name}"
|
|
555
|
+
metric = Histogram(full_name, description, labels, buckets)
|
|
556
|
+
with self._lock:
|
|
557
|
+
self._metrics[full_name] = metric
|
|
558
|
+
return metric
|
|
559
|
+
|
|
560
|
+
def get_metric(self, name: str) -> Metric | None:
|
|
561
|
+
"""Get a registered metric by name."""
|
|
562
|
+
full_name = f"{self.prefix}_{name}"
|
|
563
|
+
return self._metrics.get(full_name)
|
|
564
|
+
|
|
565
|
+
def collect_all(self) -> dict[str, list[MetricValue]]:
|
|
566
|
+
"""Collect all metric values."""
|
|
567
|
+
with self._lock:
|
|
568
|
+
return {
|
|
569
|
+
name: metric.collect()
|
|
570
|
+
for name, metric in self._metrics.items()
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
def to_prometheus(self) -> str:
|
|
574
|
+
"""Export metrics in Prometheus text format."""
|
|
575
|
+
lines = []
|
|
576
|
+
for name, metric in self._metrics.items():
|
|
577
|
+
lines.append(f"# HELP {name} {metric.description}")
|
|
578
|
+
lines.append(f"# TYPE {name} {metric.metric_type.value}")
|
|
579
|
+
|
|
580
|
+
for value in metric.collect():
|
|
581
|
+
label_str = ""
|
|
582
|
+
if value.labels:
|
|
583
|
+
label_pairs = [f'{k}="{v}"' for k, v in value.labels.items()]
|
|
584
|
+
label_str = "{" + ",".join(label_pairs) + "}"
|
|
585
|
+
lines.append(f"{name}{label_str} {value.value}")
|
|
586
|
+
|
|
587
|
+
return "\n".join(lines)
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
# =============================================================================
|
|
591
|
+
# Span Exporter Protocol
|
|
592
|
+
# =============================================================================
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
class SpanExporter(ABC):
|
|
596
|
+
"""Abstract base class for span exporters."""
|
|
597
|
+
|
|
598
|
+
@abstractmethod
|
|
599
|
+
def export(self, spans: list[Span]) -> bool:
|
|
600
|
+
"""Export spans to backend.
|
|
601
|
+
|
|
602
|
+
Args:
|
|
603
|
+
spans: Spans to export
|
|
604
|
+
|
|
605
|
+
Returns:
|
|
606
|
+
True if export succeeded
|
|
607
|
+
"""
|
|
608
|
+
pass
|
|
609
|
+
|
|
610
|
+
def shutdown(self) -> None:
|
|
611
|
+
"""Shutdown the exporter."""
|
|
612
|
+
pass
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
class ConsoleSpanExporter(SpanExporter):
|
|
616
|
+
"""Exports spans to console/logging."""
|
|
617
|
+
|
|
618
|
+
def __init__(self, logger: logging.Logger | None = None):
|
|
619
|
+
self.logger = logger or logging.getLogger("truthound.profiler.tracing")
|
|
620
|
+
|
|
621
|
+
def export(self, spans: list[Span]) -> bool:
|
|
622
|
+
for span in spans:
|
|
623
|
+
self.logger.info(
|
|
624
|
+
"Span: %s [%s] duration=%.2fms status=%s",
|
|
625
|
+
span.name,
|
|
626
|
+
span.span_id[:8],
|
|
627
|
+
span.duration_ms,
|
|
628
|
+
span.status.value,
|
|
629
|
+
)
|
|
630
|
+
for key, value in span.attributes.items():
|
|
631
|
+
self.logger.debug(" %s: %s", key, value)
|
|
632
|
+
return True
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
class InMemorySpanExporter(SpanExporter):
|
|
636
|
+
"""Exports spans to in-memory storage for testing."""
|
|
637
|
+
|
|
638
|
+
def __init__(self, max_spans: int = 10000):
|
|
639
|
+
self.max_spans = max_spans
|
|
640
|
+
self._spans: list[Span] = []
|
|
641
|
+
self._lock = threading.Lock()
|
|
642
|
+
|
|
643
|
+
def export(self, spans: list[Span]) -> bool:
|
|
644
|
+
with self._lock:
|
|
645
|
+
self._spans.extend(spans)
|
|
646
|
+
# Trim if over limit
|
|
647
|
+
if len(self._spans) > self.max_spans:
|
|
648
|
+
self._spans = self._spans[-self.max_spans:]
|
|
649
|
+
return True
|
|
650
|
+
|
|
651
|
+
def get_spans(self) -> list[Span]:
|
|
652
|
+
"""Get all exported spans."""
|
|
653
|
+
with self._lock:
|
|
654
|
+
return list(self._spans)
|
|
655
|
+
|
|
656
|
+
def clear(self) -> None:
|
|
657
|
+
"""Clear all spans."""
|
|
658
|
+
with self._lock:
|
|
659
|
+
self._spans.clear()
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
class OTLPSpanExporter(SpanExporter):
|
|
663
|
+
"""Exports spans via OTLP (OpenTelemetry Protocol).
|
|
664
|
+
|
|
665
|
+
Requires opentelemetry-exporter-otlp package.
|
|
666
|
+
"""
|
|
667
|
+
|
|
668
|
+
def __init__(
|
|
669
|
+
self,
|
|
670
|
+
endpoint: str = "http://localhost:4317",
|
|
671
|
+
headers: dict[str, str] | None = None,
|
|
672
|
+
timeout: int = 30,
|
|
673
|
+
):
|
|
674
|
+
self.endpoint = endpoint
|
|
675
|
+
self.headers = headers or {}
|
|
676
|
+
self.timeout = timeout
|
|
677
|
+
self._otlp_exporter = None
|
|
678
|
+
|
|
679
|
+
try:
|
|
680
|
+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
|
|
681
|
+
OTLPSpanExporter as _OTLPExporter,
|
|
682
|
+
)
|
|
683
|
+
self._otlp_exporter = _OTLPExporter(
|
|
684
|
+
endpoint=endpoint,
|
|
685
|
+
headers=headers,
|
|
686
|
+
timeout=timeout,
|
|
687
|
+
)
|
|
688
|
+
except ImportError:
|
|
689
|
+
pass
|
|
690
|
+
|
|
691
|
+
def export(self, spans: list[Span]) -> bool:
|
|
692
|
+
if self._otlp_exporter is None:
|
|
693
|
+
return False
|
|
694
|
+
|
|
695
|
+
# Convert to OpenTelemetry spans
|
|
696
|
+
# This is a simplified conversion - full implementation would
|
|
697
|
+
# use proper OTLP span conversion
|
|
698
|
+
return True
|
|
699
|
+
|
|
700
|
+
def shutdown(self) -> None:
|
|
701
|
+
if self._otlp_exporter:
|
|
702
|
+
self._otlp_exporter.shutdown()
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
# =============================================================================
|
|
706
|
+
# Span Exporter Registry
|
|
707
|
+
# =============================================================================
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
class SpanExporterRegistry:
|
|
711
|
+
"""Registry for span exporter factories."""
|
|
712
|
+
|
|
713
|
+
def __init__(self) -> None:
|
|
714
|
+
self._exporters: dict[str, type[SpanExporter]] = {}
|
|
715
|
+
|
|
716
|
+
def register(self, name: str, exporter_class: type[SpanExporter]) -> None:
|
|
717
|
+
"""Register an exporter class."""
|
|
718
|
+
self._exporters[name] = exporter_class
|
|
719
|
+
|
|
720
|
+
def create(self, name: str, **kwargs: Any) -> SpanExporter:
|
|
721
|
+
"""Create an exporter instance."""
|
|
722
|
+
if name not in self._exporters:
|
|
723
|
+
raise KeyError(
|
|
724
|
+
f"Unknown exporter: {name}. "
|
|
725
|
+
f"Available: {list(self._exporters.keys())}"
|
|
726
|
+
)
|
|
727
|
+
return self._exporters[name](**kwargs)
|
|
728
|
+
|
|
729
|
+
def list_exporters(self) -> list[str]:
|
|
730
|
+
"""List registered exporter names."""
|
|
731
|
+
return list(self._exporters.keys())
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
# Global registry
|
|
735
|
+
span_exporter_registry = SpanExporterRegistry()
|
|
736
|
+
span_exporter_registry.register("console", ConsoleSpanExporter)
|
|
737
|
+
span_exporter_registry.register("memory", InMemorySpanExporter)
|
|
738
|
+
span_exporter_registry.register("otlp", OTLPSpanExporter)
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
# =============================================================================
|
|
742
|
+
# Profiler Telemetry
|
|
743
|
+
# =============================================================================
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
@dataclass
|
|
747
|
+
class TelemetryConfig:
|
|
748
|
+
"""Configuration for profiler telemetry."""
|
|
749
|
+
|
|
750
|
+
service_name: str = "truthound-profiler"
|
|
751
|
+
enabled: bool = True
|
|
752
|
+
exporter: str = "console"
|
|
753
|
+
exporter_options: dict[str, Any] = field(default_factory=dict)
|
|
754
|
+
sample_rate: float = 1.0 # 1.0 = sample all
|
|
755
|
+
batch_size: int = 100
|
|
756
|
+
flush_interval_seconds: float = 5.0
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
class ProfilerTelemetry:
|
|
760
|
+
"""Main telemetry interface for the profiler.
|
|
761
|
+
|
|
762
|
+
Provides tracing, metrics, and logging integration.
|
|
763
|
+
|
|
764
|
+
Example:
|
|
765
|
+
telemetry = ProfilerTelemetry(service_name="my-profiler")
|
|
766
|
+
|
|
767
|
+
with telemetry.span("profile_table") as span:
|
|
768
|
+
span.set_attribute("table_name", "users")
|
|
769
|
+
# profiling logic...
|
|
770
|
+
"""
|
|
771
|
+
|
|
772
|
+
def __init__(
|
|
773
|
+
self,
|
|
774
|
+
service_name: str = "truthound-profiler",
|
|
775
|
+
enabled: bool = True,
|
|
776
|
+
exporter: str | SpanExporter = "console",
|
|
777
|
+
exporter_options: dict[str, Any] | None = None,
|
|
778
|
+
sample_rate: float = 1.0,
|
|
779
|
+
):
|
|
780
|
+
self.service_name = service_name
|
|
781
|
+
self.enabled = enabled
|
|
782
|
+
self.sample_rate = sample_rate
|
|
783
|
+
self._lock = threading.Lock()
|
|
784
|
+
|
|
785
|
+
# Initialize exporter
|
|
786
|
+
if isinstance(exporter, SpanExporter):
|
|
787
|
+
self._exporter = exporter
|
|
788
|
+
else:
|
|
789
|
+
options = exporter_options or {}
|
|
790
|
+
self._exporter = span_exporter_registry.create(exporter, **options)
|
|
791
|
+
|
|
792
|
+
# Span tracking
|
|
793
|
+
self._spans: list[Span] = []
|
|
794
|
+
self._current_span: Span | None = None
|
|
795
|
+
self._span_stack: list[Span] = []
|
|
796
|
+
|
|
797
|
+
# Metrics collector
|
|
798
|
+
self.metrics = MetricsCollector()
|
|
799
|
+
|
|
800
|
+
# Generate IDs
|
|
801
|
+
self._trace_id_counter = 0
|
|
802
|
+
self._span_id_counter = 0
|
|
803
|
+
|
|
804
|
+
def _generate_trace_id(self) -> str:
|
|
805
|
+
"""Generate unique trace ID."""
|
|
806
|
+
with self._lock:
|
|
807
|
+
self._trace_id_counter += 1
|
|
808
|
+
return f"{self._trace_id_counter:032x}"
|
|
809
|
+
|
|
810
|
+
def _generate_span_id(self) -> str:
|
|
811
|
+
"""Generate unique span ID."""
|
|
812
|
+
with self._lock:
|
|
813
|
+
self._span_id_counter += 1
|
|
814
|
+
return f"{self._span_id_counter:016x}"
|
|
815
|
+
|
|
816
|
+
def _should_sample(self) -> bool:
|
|
817
|
+
"""Determine if this trace should be sampled."""
|
|
818
|
+
import random
|
|
819
|
+
return random.random() < self.sample_rate
|
|
820
|
+
|
|
821
|
+
@contextmanager
|
|
822
|
+
def span(
|
|
823
|
+
self,
|
|
824
|
+
name: str,
|
|
825
|
+
attributes: dict[str, Any] | None = None,
|
|
826
|
+
) -> Generator[Span, None, None]:
|
|
827
|
+
"""Create a span context manager.
|
|
828
|
+
|
|
829
|
+
Args:
|
|
830
|
+
name: Span name
|
|
831
|
+
attributes: Initial attributes
|
|
832
|
+
|
|
833
|
+
Yields:
|
|
834
|
+
Span object for the duration
|
|
835
|
+
"""
|
|
836
|
+
if not self.enabled or not self._should_sample():
|
|
837
|
+
# Return a no-op span
|
|
838
|
+
yield Span(
|
|
839
|
+
name=name,
|
|
840
|
+
trace_id="",
|
|
841
|
+
span_id="",
|
|
842
|
+
)
|
|
843
|
+
return
|
|
844
|
+
|
|
845
|
+
# Create span
|
|
846
|
+
parent_id = self._span_stack[-1].span_id if self._span_stack else None
|
|
847
|
+
trace_id = (
|
|
848
|
+
self._span_stack[0].trace_id if self._span_stack
|
|
849
|
+
else self._generate_trace_id()
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
span = Span(
|
|
853
|
+
name=name,
|
|
854
|
+
trace_id=trace_id,
|
|
855
|
+
span_id=self._generate_span_id(),
|
|
856
|
+
parent_id=parent_id,
|
|
857
|
+
attributes=attributes or {},
|
|
858
|
+
)
|
|
859
|
+
|
|
860
|
+
self._span_stack.append(span)
|
|
861
|
+
|
|
862
|
+
try:
|
|
863
|
+
yield span
|
|
864
|
+
if span.status == SpanStatus.UNSET:
|
|
865
|
+
span.set_status(SpanStatus.OK)
|
|
866
|
+
except Exception as e:
|
|
867
|
+
span.record_exception(e)
|
|
868
|
+
raise
|
|
869
|
+
finally:
|
|
870
|
+
span.end()
|
|
871
|
+
self._span_stack.pop()
|
|
872
|
+
self._spans.append(span)
|
|
873
|
+
|
|
874
|
+
# Export if batch is full
|
|
875
|
+
if len(self._spans) >= 100:
|
|
876
|
+
self._flush()
|
|
877
|
+
|
|
878
|
+
def _flush(self) -> None:
|
|
879
|
+
"""Flush pending spans to exporter."""
|
|
880
|
+
with self._lock:
|
|
881
|
+
if self._spans:
|
|
882
|
+
self._exporter.export(list(self._spans))
|
|
883
|
+
self._spans.clear()
|
|
884
|
+
|
|
885
|
+
def shutdown(self) -> None:
|
|
886
|
+
"""Shutdown telemetry and flush remaining spans."""
|
|
887
|
+
self._flush()
|
|
888
|
+
self._exporter.shutdown()
|
|
889
|
+
|
|
890
|
+
def record_profile(
|
|
891
|
+
self,
|
|
892
|
+
profile_type: str,
|
|
893
|
+
duration_seconds: float,
|
|
894
|
+
row_count: int,
|
|
895
|
+
column_count: int,
|
|
896
|
+
success: bool = True,
|
|
897
|
+
) -> None:
|
|
898
|
+
"""Record profile metrics.
|
|
899
|
+
|
|
900
|
+
Args:
|
|
901
|
+
profile_type: Type of profile (table, column, etc.)
|
|
902
|
+
duration_seconds: Time taken
|
|
903
|
+
row_count: Number of rows profiled
|
|
904
|
+
column_count: Number of columns profiled
|
|
905
|
+
success: Whether profile succeeded
|
|
906
|
+
"""
|
|
907
|
+
status = "success" if success else "error"
|
|
908
|
+
|
|
909
|
+
self.metrics.profiles_total.inc(status=status, type=profile_type)
|
|
910
|
+
self.metrics.profile_duration.observe(
|
|
911
|
+
duration_seconds,
|
|
912
|
+
operation=profile_type,
|
|
913
|
+
)
|
|
914
|
+
self.metrics.rows_processed.observe(row_count)
|
|
915
|
+
|
|
916
|
+
def record_column_profile(
|
|
917
|
+
self,
|
|
918
|
+
column_type: str,
|
|
919
|
+
duration_seconds: float,
|
|
920
|
+
) -> None:
|
|
921
|
+
"""Record column profile metrics."""
|
|
922
|
+
self.metrics.columns_profiled.inc(data_type=column_type)
|
|
923
|
+
self.metrics.column_profile_duration.observe(
|
|
924
|
+
duration_seconds,
|
|
925
|
+
column_type=column_type,
|
|
926
|
+
)
|
|
927
|
+
|
|
928
|
+
def record_cache_operation(
|
|
929
|
+
self,
|
|
930
|
+
operation: str,
|
|
931
|
+
hit: bool,
|
|
932
|
+
) -> None:
|
|
933
|
+
"""Record cache operation."""
|
|
934
|
+
result = "hit" if hit else "miss"
|
|
935
|
+
self.metrics.cache_operations.inc(operation=operation, result=result)
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
# =============================================================================
|
|
939
|
+
# Decorators
|
|
940
|
+
# =============================================================================
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
# Global telemetry instance (can be overridden)
|
|
944
|
+
_global_telemetry: ProfilerTelemetry | None = None
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
def get_telemetry() -> ProfilerTelemetry:
|
|
948
|
+
"""Get or create global telemetry instance."""
|
|
949
|
+
global _global_telemetry
|
|
950
|
+
if _global_telemetry is None:
|
|
951
|
+
_global_telemetry = ProfilerTelemetry()
|
|
952
|
+
return _global_telemetry
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
def set_telemetry(telemetry: ProfilerTelemetry) -> None:
|
|
956
|
+
"""Set global telemetry instance."""
|
|
957
|
+
global _global_telemetry
|
|
958
|
+
_global_telemetry = telemetry
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
def traced(
|
|
962
|
+
name: str | None = None,
|
|
963
|
+
attributes: dict[str, Any] | None = None,
|
|
964
|
+
record_args: bool = False,
|
|
965
|
+
) -> Callable[[F], F]:
|
|
966
|
+
"""Decorator to automatically trace a function.
|
|
967
|
+
|
|
968
|
+
Example:
|
|
969
|
+
@traced("profile_column")
|
|
970
|
+
def profile_column(name: str) -> ColumnProfile:
|
|
971
|
+
...
|
|
972
|
+
|
|
973
|
+
@traced(record_args=True)
|
|
974
|
+
def process_data(data: list) -> None:
|
|
975
|
+
...
|
|
976
|
+
|
|
977
|
+
Args:
|
|
978
|
+
name: Span name (defaults to function name)
|
|
979
|
+
attributes: Static attributes to add
|
|
980
|
+
record_args: Whether to record function arguments
|
|
981
|
+
|
|
982
|
+
Returns:
|
|
983
|
+
Decorated function
|
|
984
|
+
"""
|
|
985
|
+
def decorator(func: F) -> F:
|
|
986
|
+
span_name = name or func.__name__
|
|
987
|
+
|
|
988
|
+
@wraps(func)
|
|
989
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
990
|
+
telemetry = get_telemetry()
|
|
991
|
+
|
|
992
|
+
span_attrs = dict(attributes or {})
|
|
993
|
+
span_attrs["function"] = func.__name__
|
|
994
|
+
span_attrs["module"] = func.__module__
|
|
995
|
+
|
|
996
|
+
if record_args:
|
|
997
|
+
span_attrs["args_count"] = len(args)
|
|
998
|
+
span_attrs["kwargs_keys"] = list(kwargs.keys())
|
|
999
|
+
|
|
1000
|
+
with telemetry.span(span_name, attributes=span_attrs) as span:
|
|
1001
|
+
result = func(*args, **kwargs)
|
|
1002
|
+
|
|
1003
|
+
# Add result info if available
|
|
1004
|
+
if hasattr(result, "__len__"):
|
|
1005
|
+
span.set_attribute("result_length", len(result))
|
|
1006
|
+
|
|
1007
|
+
return result
|
|
1008
|
+
|
|
1009
|
+
return wrapper # type: ignore
|
|
1010
|
+
|
|
1011
|
+
return decorator
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
def timed(
|
|
1015
|
+
histogram: Histogram | None = None,
|
|
1016
|
+
metric_name: str = "operation_duration_seconds",
|
|
1017
|
+
**labels: str,
|
|
1018
|
+
) -> Callable[[F], F]:
|
|
1019
|
+
"""Decorator to time a function and record to histogram.
|
|
1020
|
+
|
|
1021
|
+
Example:
|
|
1022
|
+
@timed(metric_name="profile_column_seconds", column_type="string")
|
|
1023
|
+
def profile_string_column(col):
|
|
1024
|
+
...
|
|
1025
|
+
|
|
1026
|
+
Args:
|
|
1027
|
+
histogram: Histogram to record to
|
|
1028
|
+
metric_name: Metric name if creating new histogram
|
|
1029
|
+
**labels: Labels to add to metric
|
|
1030
|
+
|
|
1031
|
+
Returns:
|
|
1032
|
+
Decorated function
|
|
1033
|
+
"""
|
|
1034
|
+
def decorator(func: F) -> F:
|
|
1035
|
+
@wraps(func)
|
|
1036
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
1037
|
+
hist = histogram
|
|
1038
|
+
if hist is None:
|
|
1039
|
+
telemetry = get_telemetry()
|
|
1040
|
+
hist = telemetry.metrics.get_metric(metric_name)
|
|
1041
|
+
if hist is None or not isinstance(hist, Histogram):
|
|
1042
|
+
hist = telemetry.metrics.register_histogram(metric_name)
|
|
1043
|
+
|
|
1044
|
+
start = time.perf_counter()
|
|
1045
|
+
try:
|
|
1046
|
+
return func(*args, **kwargs)
|
|
1047
|
+
finally:
|
|
1048
|
+
duration = time.perf_counter() - start
|
|
1049
|
+
hist.observe(duration, **labels)
|
|
1050
|
+
|
|
1051
|
+
return wrapper # type: ignore
|
|
1052
|
+
|
|
1053
|
+
return decorator
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
# =============================================================================
|
|
1057
|
+
# OpenTelemetry Integration
|
|
1058
|
+
# =============================================================================
|
|
1059
|
+
|
|
1060
|
+
|
|
1061
|
+
class OpenTelemetryIntegration:
|
|
1062
|
+
"""Integration with OpenTelemetry SDK.
|
|
1063
|
+
|
|
1064
|
+
Wraps OpenTelemetry tracer and meter for seamless integration.
|
|
1065
|
+
|
|
1066
|
+
Example:
|
|
1067
|
+
# If opentelemetry is installed
|
|
1068
|
+
otel = OpenTelemetryIntegration.create(
|
|
1069
|
+
service_name="truthound-profiler",
|
|
1070
|
+
endpoint="http://localhost:4317"
|
|
1071
|
+
)
|
|
1072
|
+
|
|
1073
|
+
with otel.tracer.start_as_current_span("profile") as span:
|
|
1074
|
+
span.set_attribute("table", "users")
|
|
1075
|
+
...
|
|
1076
|
+
"""
|
|
1077
|
+
|
|
1078
|
+
def __init__(
|
|
1079
|
+
self,
|
|
1080
|
+
tracer: Any = None,
|
|
1081
|
+
meter: Any = None,
|
|
1082
|
+
):
|
|
1083
|
+
self._tracer = tracer
|
|
1084
|
+
self._meter = meter
|
|
1085
|
+
|
|
1086
|
+
@classmethod
|
|
1087
|
+
def create(
|
|
1088
|
+
cls,
|
|
1089
|
+
service_name: str = "truthound-profiler",
|
|
1090
|
+
endpoint: str | None = None,
|
|
1091
|
+
) -> "OpenTelemetryIntegration":
|
|
1092
|
+
"""Create OpenTelemetry integration.
|
|
1093
|
+
|
|
1094
|
+
Args:
|
|
1095
|
+
service_name: Service name for tracing
|
|
1096
|
+
endpoint: OTLP endpoint (optional)
|
|
1097
|
+
|
|
1098
|
+
Returns:
|
|
1099
|
+
Configured integration
|
|
1100
|
+
"""
|
|
1101
|
+
tracer = None
|
|
1102
|
+
meter = None
|
|
1103
|
+
|
|
1104
|
+
try:
|
|
1105
|
+
from opentelemetry import trace, metrics
|
|
1106
|
+
from opentelemetry.sdk.resources import Resource
|
|
1107
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
1108
|
+
from opentelemetry.sdk.metrics import MeterProvider
|
|
1109
|
+
|
|
1110
|
+
resource = Resource.create({"service.name": service_name})
|
|
1111
|
+
|
|
1112
|
+
# Setup tracing
|
|
1113
|
+
trace_provider = TracerProvider(resource=resource)
|
|
1114
|
+
trace.set_tracer_provider(trace_provider)
|
|
1115
|
+
|
|
1116
|
+
if endpoint:
|
|
1117
|
+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
|
|
1118
|
+
OTLPSpanExporter,
|
|
1119
|
+
)
|
|
1120
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
1121
|
+
|
|
1122
|
+
exporter = OTLPSpanExporter(endpoint=endpoint)
|
|
1123
|
+
trace_provider.add_span_processor(BatchSpanProcessor(exporter))
|
|
1124
|
+
|
|
1125
|
+
tracer = trace.get_tracer(service_name)
|
|
1126
|
+
|
|
1127
|
+
# Setup metrics
|
|
1128
|
+
meter_provider = MeterProvider(resource=resource)
|
|
1129
|
+
metrics.set_meter_provider(meter_provider)
|
|
1130
|
+
meter = metrics.get_meter(service_name)
|
|
1131
|
+
|
|
1132
|
+
except ImportError:
|
|
1133
|
+
pass
|
|
1134
|
+
|
|
1135
|
+
return cls(tracer=tracer, meter=meter)
|
|
1136
|
+
|
|
1137
|
+
@property
|
|
1138
|
+
def tracer(self) -> Any:
|
|
1139
|
+
"""Get OpenTelemetry tracer."""
|
|
1140
|
+
return self._tracer
|
|
1141
|
+
|
|
1142
|
+
@property
|
|
1143
|
+
def meter(self) -> Any:
|
|
1144
|
+
"""Get OpenTelemetry meter."""
|
|
1145
|
+
return self._meter
|
|
1146
|
+
|
|
1147
|
+
@property
|
|
1148
|
+
def available(self) -> bool:
|
|
1149
|
+
"""Check if OpenTelemetry is available."""
|
|
1150
|
+
return self._tracer is not None
|
|
1151
|
+
|
|
1152
|
+
|
|
1153
|
+
# =============================================================================
|
|
1154
|
+
# Convenience Functions
|
|
1155
|
+
# =============================================================================
|
|
1156
|
+
|
|
1157
|
+
|
|
1158
|
+
def create_telemetry(
|
|
1159
|
+
service_name: str = "truthound-profiler",
|
|
1160
|
+
exporter: str = "console",
|
|
1161
|
+
**kwargs: Any,
|
|
1162
|
+
) -> ProfilerTelemetry:
|
|
1163
|
+
"""Create and configure profiler telemetry.
|
|
1164
|
+
|
|
1165
|
+
Args:
|
|
1166
|
+
service_name: Service name for tracing
|
|
1167
|
+
exporter: Exporter type
|
|
1168
|
+
**kwargs: Exporter options
|
|
1169
|
+
|
|
1170
|
+
Returns:
|
|
1171
|
+
Configured ProfilerTelemetry instance
|
|
1172
|
+
"""
|
|
1173
|
+
telemetry = ProfilerTelemetry(
|
|
1174
|
+
service_name=service_name,
|
|
1175
|
+
exporter=exporter,
|
|
1176
|
+
exporter_options=kwargs,
|
|
1177
|
+
)
|
|
1178
|
+
set_telemetry(telemetry)
|
|
1179
|
+
return telemetry
|
|
1180
|
+
|
|
1181
|
+
|
|
1182
|
+
def get_metrics() -> MetricsCollector:
|
|
1183
|
+
"""Get the global metrics collector."""
|
|
1184
|
+
return get_telemetry().metrics
|