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,1157 @@
|
|
|
1
|
+
"""Metrics collection system for Truthound.
|
|
2
|
+
|
|
3
|
+
This module provides a flexible metrics collection framework with support
|
|
4
|
+
for multiple backend exporters (Prometheus, StatsD, OpenTelemetry).
|
|
5
|
+
|
|
6
|
+
Metric Types:
|
|
7
|
+
- Counter: Monotonically increasing value (e.g., request count)
|
|
8
|
+
- Gauge: Point-in-time value (e.g., active connections)
|
|
9
|
+
- Histogram: Distribution of values with buckets
|
|
10
|
+
- Summary: Distribution with quantiles
|
|
11
|
+
|
|
12
|
+
Design Principles:
|
|
13
|
+
1. Label-based: Dimensional metrics with key-value labels
|
|
14
|
+
2. Backend agnostic: Same API for all exporters
|
|
15
|
+
3. Lazy registration: Metrics created on first use
|
|
16
|
+
4. Thread-safe: All operations are thread-safe
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import threading
|
|
22
|
+
import time
|
|
23
|
+
from abc import ABC, abstractmethod
|
|
24
|
+
from contextlib import contextmanager
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from datetime import datetime
|
|
27
|
+
from enum import Enum
|
|
28
|
+
from typing import Any, Callable, Iterator, TypeVar
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# =============================================================================
|
|
32
|
+
# Metric Types
|
|
33
|
+
# =============================================================================
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class MetricType(Enum):
|
|
37
|
+
"""Types of metrics."""
|
|
38
|
+
|
|
39
|
+
COUNTER = "counter"
|
|
40
|
+
GAUGE = "gauge"
|
|
41
|
+
HISTOGRAM = "histogram"
|
|
42
|
+
SUMMARY = "summary"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class MetricKey:
|
|
47
|
+
"""Unique identifier for a metric with labels."""
|
|
48
|
+
|
|
49
|
+
name: str
|
|
50
|
+
labels: tuple[tuple[str, str], ...]
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def create(cls, name: str, labels: dict[str, str] | None = None) -> "MetricKey":
|
|
54
|
+
"""Create metric key from name and labels."""
|
|
55
|
+
label_tuple = tuple(sorted((labels or {}).items()))
|
|
56
|
+
return cls(name=name, labels=label_tuple)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class MetricMetadata:
|
|
61
|
+
"""Metadata about a metric."""
|
|
62
|
+
|
|
63
|
+
name: str
|
|
64
|
+
type: MetricType
|
|
65
|
+
description: str
|
|
66
|
+
unit: str = ""
|
|
67
|
+
label_names: tuple[str, ...] = ()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# =============================================================================
|
|
71
|
+
# Metric Base Class
|
|
72
|
+
# =============================================================================
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Metric(ABC):
|
|
76
|
+
"""Abstract base class for metrics.
|
|
77
|
+
|
|
78
|
+
All metrics support labels (dimensions) for multi-dimensional analysis.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
name: str,
|
|
84
|
+
description: str = "",
|
|
85
|
+
*,
|
|
86
|
+
unit: str = "",
|
|
87
|
+
label_names: tuple[str, ...] | list[str] = (),
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Initialize metric.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
name: Metric name (should be lowercase with underscores).
|
|
93
|
+
description: Human-readable description.
|
|
94
|
+
unit: Unit of measurement.
|
|
95
|
+
label_names: Names of labels for this metric.
|
|
96
|
+
"""
|
|
97
|
+
self._name = name
|
|
98
|
+
self._description = description
|
|
99
|
+
self._unit = unit
|
|
100
|
+
self._label_names = tuple(label_names)
|
|
101
|
+
self._lock = threading.Lock()
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def name(self) -> str:
|
|
105
|
+
"""Get metric name."""
|
|
106
|
+
return self._name
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def description(self) -> str:
|
|
110
|
+
"""Get metric description."""
|
|
111
|
+
return self._description
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
@abstractmethod
|
|
115
|
+
def type(self) -> MetricType:
|
|
116
|
+
"""Get metric type."""
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def metadata(self) -> MetricMetadata:
|
|
121
|
+
"""Get metric metadata."""
|
|
122
|
+
return MetricMetadata(
|
|
123
|
+
name=self._name,
|
|
124
|
+
type=self.type,
|
|
125
|
+
description=self._description,
|
|
126
|
+
unit=self._unit,
|
|
127
|
+
label_names=self._label_names,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def _validate_labels(self, labels: dict[str, str]) -> None:
|
|
131
|
+
"""Validate label names match expected."""
|
|
132
|
+
if set(labels.keys()) != set(self._label_names):
|
|
133
|
+
expected = set(self._label_names)
|
|
134
|
+
actual = set(labels.keys())
|
|
135
|
+
raise ValueError(
|
|
136
|
+
f"Label mismatch for '{self._name}': "
|
|
137
|
+
f"expected {expected}, got {actual}"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
@abstractmethod
|
|
141
|
+
def collect(self) -> list[tuple[dict[str, str], float]]:
|
|
142
|
+
"""Collect all metric values with labels.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
List of (labels, value) tuples.
|
|
146
|
+
"""
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# =============================================================================
|
|
151
|
+
# Counter
|
|
152
|
+
# =============================================================================
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class Counter(Metric):
|
|
156
|
+
"""Monotonically increasing counter.
|
|
157
|
+
|
|
158
|
+
Counters only go up (and reset to zero on restart).
|
|
159
|
+
Use for: request counts, errors, completed tasks.
|
|
160
|
+
|
|
161
|
+
Example:
|
|
162
|
+
>>> requests = Counter("http_requests_total", "Total HTTP requests")
|
|
163
|
+
>>> requests.inc()
|
|
164
|
+
>>> requests.add(5)
|
|
165
|
+
>>> requests.labels(method="GET", status="200").inc()
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
def __init__(
|
|
169
|
+
self,
|
|
170
|
+
name: str,
|
|
171
|
+
description: str = "",
|
|
172
|
+
*,
|
|
173
|
+
labels: tuple[str, ...] | list[str] = (),
|
|
174
|
+
**kwargs: Any,
|
|
175
|
+
) -> None:
|
|
176
|
+
"""Initialize counter.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
name: Counter name.
|
|
180
|
+
description: Human-readable description.
|
|
181
|
+
labels: Label names for this counter.
|
|
182
|
+
**kwargs: Additional arguments for Metric.
|
|
183
|
+
"""
|
|
184
|
+
super().__init__(name, description, label_names=labels, **kwargs)
|
|
185
|
+
self._values: dict[MetricKey, float] = {}
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def type(self) -> MetricType:
|
|
189
|
+
return MetricType.COUNTER
|
|
190
|
+
|
|
191
|
+
def inc(self, value: float = 1.0, **labels: str) -> None:
|
|
192
|
+
"""Increment counter.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
value: Amount to increment (must be positive).
|
|
196
|
+
**labels: Label values.
|
|
197
|
+
"""
|
|
198
|
+
if value < 0:
|
|
199
|
+
raise ValueError("Counter can only be incremented")
|
|
200
|
+
self.add(value, **labels)
|
|
201
|
+
|
|
202
|
+
def add(self, value: float, **labels: str) -> None:
|
|
203
|
+
"""Add to counter.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
value: Amount to add (must be positive).
|
|
207
|
+
**labels: Label values.
|
|
208
|
+
"""
|
|
209
|
+
if value < 0:
|
|
210
|
+
raise ValueError("Counter can only increase")
|
|
211
|
+
|
|
212
|
+
if self._label_names:
|
|
213
|
+
self._validate_labels(labels)
|
|
214
|
+
|
|
215
|
+
key = MetricKey.create(self._name, labels)
|
|
216
|
+
|
|
217
|
+
with self._lock:
|
|
218
|
+
self._values[key] = self._values.get(key, 0.0) + value
|
|
219
|
+
|
|
220
|
+
def labels(self, **labels: str) -> "LabeledCounter":
|
|
221
|
+
"""Get counter with specific labels.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
**labels: Label values.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
LabeledCounter for the specific label set.
|
|
228
|
+
"""
|
|
229
|
+
return LabeledCounter(self, labels)
|
|
230
|
+
|
|
231
|
+
def get(self, **labels: str) -> float:
|
|
232
|
+
"""Get current counter value.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
**labels: Label values.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Current value.
|
|
239
|
+
"""
|
|
240
|
+
key = MetricKey.create(self._name, labels)
|
|
241
|
+
with self._lock:
|
|
242
|
+
return self._values.get(key, 0.0)
|
|
243
|
+
|
|
244
|
+
def collect(self) -> list[tuple[dict[str, str], float]]:
|
|
245
|
+
"""Collect all counter values."""
|
|
246
|
+
with self._lock:
|
|
247
|
+
return [
|
|
248
|
+
(dict(key.labels), value)
|
|
249
|
+
for key, value in self._values.items()
|
|
250
|
+
]
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class LabeledCounter:
|
|
254
|
+
"""Counter with pre-set labels."""
|
|
255
|
+
|
|
256
|
+
def __init__(self, counter: Counter, labels: dict[str, str]) -> None:
|
|
257
|
+
self._counter = counter
|
|
258
|
+
self._labels = labels
|
|
259
|
+
|
|
260
|
+
def inc(self, value: float = 1.0) -> None:
|
|
261
|
+
"""Increment counter."""
|
|
262
|
+
self._counter.inc(value, **self._labels)
|
|
263
|
+
|
|
264
|
+
def add(self, value: float) -> None:
|
|
265
|
+
"""Add to counter."""
|
|
266
|
+
self._counter.add(value, **self._labels)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# =============================================================================
|
|
270
|
+
# Gauge
|
|
271
|
+
# =============================================================================
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class Gauge(Metric):
|
|
275
|
+
"""Point-in-time value that can go up or down.
|
|
276
|
+
|
|
277
|
+
Use for: temperature, queue size, memory usage.
|
|
278
|
+
|
|
279
|
+
Example:
|
|
280
|
+
>>> temperature = Gauge("temperature_celsius", "Current temperature")
|
|
281
|
+
>>> temperature.set(23.5)
|
|
282
|
+
>>> temperature.inc()
|
|
283
|
+
>>> temperature.dec(0.5)
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
def __init__(
|
|
287
|
+
self,
|
|
288
|
+
name: str,
|
|
289
|
+
description: str = "",
|
|
290
|
+
*,
|
|
291
|
+
labels: tuple[str, ...] | list[str] = (),
|
|
292
|
+
**kwargs: Any,
|
|
293
|
+
) -> None:
|
|
294
|
+
"""Initialize gauge."""
|
|
295
|
+
super().__init__(name, description, label_names=labels, **kwargs)
|
|
296
|
+
self._values: dict[MetricKey, float] = {}
|
|
297
|
+
|
|
298
|
+
@property
|
|
299
|
+
def type(self) -> MetricType:
|
|
300
|
+
return MetricType.GAUGE
|
|
301
|
+
|
|
302
|
+
def set(self, value: float, **labels: str) -> None:
|
|
303
|
+
"""Set gauge value.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
value: New value.
|
|
307
|
+
**labels: Label values.
|
|
308
|
+
"""
|
|
309
|
+
if self._label_names:
|
|
310
|
+
self._validate_labels(labels)
|
|
311
|
+
|
|
312
|
+
key = MetricKey.create(self._name, labels)
|
|
313
|
+
|
|
314
|
+
with self._lock:
|
|
315
|
+
self._values[key] = value
|
|
316
|
+
|
|
317
|
+
def inc(self, value: float = 1.0, **labels: str) -> None:
|
|
318
|
+
"""Increment gauge.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
value: Amount to increment.
|
|
322
|
+
**labels: Label values.
|
|
323
|
+
"""
|
|
324
|
+
key = MetricKey.create(self._name, labels if self._label_names else {})
|
|
325
|
+
|
|
326
|
+
with self._lock:
|
|
327
|
+
self._values[key] = self._values.get(key, 0.0) + value
|
|
328
|
+
|
|
329
|
+
def dec(self, value: float = 1.0, **labels: str) -> None:
|
|
330
|
+
"""Decrement gauge.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
value: Amount to decrement.
|
|
334
|
+
**labels: Label values.
|
|
335
|
+
"""
|
|
336
|
+
self.inc(-value, **labels)
|
|
337
|
+
|
|
338
|
+
def get(self, **labels: str) -> float:
|
|
339
|
+
"""Get current gauge value."""
|
|
340
|
+
key = MetricKey.create(self._name, labels)
|
|
341
|
+
with self._lock:
|
|
342
|
+
return self._values.get(key, 0.0)
|
|
343
|
+
|
|
344
|
+
def labels(self, **labels: str) -> "LabeledGauge":
|
|
345
|
+
"""Get gauge with specific labels."""
|
|
346
|
+
return LabeledGauge(self, labels)
|
|
347
|
+
|
|
348
|
+
@contextmanager
|
|
349
|
+
def track_inprogress(self, **labels: str) -> Iterator[None]:
|
|
350
|
+
"""Track in-progress operations.
|
|
351
|
+
|
|
352
|
+
Increments on entry, decrements on exit.
|
|
353
|
+
"""
|
|
354
|
+
self.inc(**labels)
|
|
355
|
+
try:
|
|
356
|
+
yield
|
|
357
|
+
finally:
|
|
358
|
+
self.dec(**labels)
|
|
359
|
+
|
|
360
|
+
def set_to_current_time(self, **labels: str) -> None:
|
|
361
|
+
"""Set gauge to current Unix timestamp."""
|
|
362
|
+
self.set(time.time(), **labels)
|
|
363
|
+
|
|
364
|
+
def collect(self) -> list[tuple[dict[str, str], float]]:
|
|
365
|
+
"""Collect all gauge values."""
|
|
366
|
+
with self._lock:
|
|
367
|
+
return [
|
|
368
|
+
(dict(key.labels), value)
|
|
369
|
+
for key, value in self._values.items()
|
|
370
|
+
]
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class LabeledGauge:
|
|
374
|
+
"""Gauge with pre-set labels."""
|
|
375
|
+
|
|
376
|
+
def __init__(self, gauge: Gauge, labels: dict[str, str]) -> None:
|
|
377
|
+
self._gauge = gauge
|
|
378
|
+
self._labels = labels
|
|
379
|
+
|
|
380
|
+
def set(self, value: float) -> None:
|
|
381
|
+
"""Set gauge value."""
|
|
382
|
+
self._gauge.set(value, **self._labels)
|
|
383
|
+
|
|
384
|
+
def inc(self, value: float = 1.0) -> None:
|
|
385
|
+
"""Increment gauge."""
|
|
386
|
+
self._gauge.inc(value, **self._labels)
|
|
387
|
+
|
|
388
|
+
def dec(self, value: float = 1.0) -> None:
|
|
389
|
+
"""Decrement gauge."""
|
|
390
|
+
self._gauge.dec(value, **self._labels)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
# =============================================================================
|
|
394
|
+
# Histogram
|
|
395
|
+
# =============================================================================
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
class Histogram(Metric):
|
|
399
|
+
"""Distribution of values with configurable buckets.
|
|
400
|
+
|
|
401
|
+
Use for: request latency, response sizes.
|
|
402
|
+
|
|
403
|
+
Example:
|
|
404
|
+
>>> latency = Histogram(
|
|
405
|
+
... "request_duration_seconds",
|
|
406
|
+
... "Request latency",
|
|
407
|
+
... buckets=[0.01, 0.05, 0.1, 0.5, 1.0, 5.0],
|
|
408
|
+
... )
|
|
409
|
+
>>> latency.observe(0.42)
|
|
410
|
+
>>> with latency.time():
|
|
411
|
+
... process_request()
|
|
412
|
+
"""
|
|
413
|
+
|
|
414
|
+
DEFAULT_BUCKETS = (
|
|
415
|
+
0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5,
|
|
416
|
+
0.75, 1.0, 2.5, 5.0, 7.5, 10.0, float("inf"),
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
def __init__(
|
|
420
|
+
self,
|
|
421
|
+
name: str,
|
|
422
|
+
description: str = "",
|
|
423
|
+
*,
|
|
424
|
+
buckets: tuple[float, ...] | list[float] | None = None,
|
|
425
|
+
labels: tuple[str, ...] | list[str] = (),
|
|
426
|
+
**kwargs: Any,
|
|
427
|
+
) -> None:
|
|
428
|
+
"""Initialize histogram.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
name: Histogram name.
|
|
432
|
+
description: Human-readable description.
|
|
433
|
+
buckets: Upper bounds for buckets.
|
|
434
|
+
labels: Label names.
|
|
435
|
+
**kwargs: Additional arguments.
|
|
436
|
+
"""
|
|
437
|
+
super().__init__(name, description, label_names=labels, **kwargs)
|
|
438
|
+
self._buckets = tuple(sorted(buckets or self.DEFAULT_BUCKETS))
|
|
439
|
+
|
|
440
|
+
# Ensure +Inf is included
|
|
441
|
+
if self._buckets[-1] != float("inf"):
|
|
442
|
+
self._buckets = (*self._buckets, float("inf"))
|
|
443
|
+
|
|
444
|
+
# Storage: key -> (bucket_counts, sum, count)
|
|
445
|
+
self._data: dict[MetricKey, tuple[list[int], float, int]] = {}
|
|
446
|
+
|
|
447
|
+
@property
|
|
448
|
+
def type(self) -> MetricType:
|
|
449
|
+
return MetricType.HISTOGRAM
|
|
450
|
+
|
|
451
|
+
@property
|
|
452
|
+
def buckets(self) -> tuple[float, ...]:
|
|
453
|
+
"""Get bucket boundaries."""
|
|
454
|
+
return self._buckets
|
|
455
|
+
|
|
456
|
+
def _get_data(self, key: MetricKey) -> tuple[list[int], float, int]:
|
|
457
|
+
"""Get or create data for a key."""
|
|
458
|
+
if key not in self._data:
|
|
459
|
+
self._data[key] = ([0] * len(self._buckets), 0.0, 0)
|
|
460
|
+
return self._data[key]
|
|
461
|
+
|
|
462
|
+
def observe(self, value: float, **labels: str) -> None:
|
|
463
|
+
"""Observe a value.
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
value: Observed value.
|
|
467
|
+
**labels: Label values.
|
|
468
|
+
"""
|
|
469
|
+
if self._label_names:
|
|
470
|
+
self._validate_labels(labels)
|
|
471
|
+
|
|
472
|
+
key = MetricKey.create(self._name, labels)
|
|
473
|
+
|
|
474
|
+
with self._lock:
|
|
475
|
+
bucket_counts, total_sum, count = self._get_data(key)
|
|
476
|
+
|
|
477
|
+
# Find the bucket this value belongs to (first bucket where value <= bound)
|
|
478
|
+
# Only increment that one bucket - cumulative is computed in collect()
|
|
479
|
+
for i, bound in enumerate(self._buckets):
|
|
480
|
+
if value <= bound:
|
|
481
|
+
bucket_counts[i] += 1
|
|
482
|
+
break
|
|
483
|
+
|
|
484
|
+
# Update sum and count
|
|
485
|
+
self._data[key] = (bucket_counts, total_sum + value, count + 1)
|
|
486
|
+
|
|
487
|
+
@contextmanager
|
|
488
|
+
def time(self, **labels: str) -> Iterator[None]:
|
|
489
|
+
"""Context manager to measure duration.
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
**labels: Label values.
|
|
493
|
+
"""
|
|
494
|
+
start = time.perf_counter()
|
|
495
|
+
try:
|
|
496
|
+
yield
|
|
497
|
+
finally:
|
|
498
|
+
duration = time.perf_counter() - start
|
|
499
|
+
self.observe(duration, **labels)
|
|
500
|
+
|
|
501
|
+
def labels(self, **labels: str) -> "LabeledHistogram":
|
|
502
|
+
"""Get histogram with specific labels."""
|
|
503
|
+
return LabeledHistogram(self, labels)
|
|
504
|
+
|
|
505
|
+
def collect(self) -> list[tuple[dict[str, str], dict[str, Any]]]:
|
|
506
|
+
"""Collect histogram data.
|
|
507
|
+
|
|
508
|
+
Returns list of (labels, data) where data contains:
|
|
509
|
+
- buckets: dict mapping bound to cumulative count
|
|
510
|
+
- sum: total of all observed values
|
|
511
|
+
- count: number of observations
|
|
512
|
+
"""
|
|
513
|
+
with self._lock:
|
|
514
|
+
results = []
|
|
515
|
+
for key, (bucket_counts, total_sum, count) in self._data.items():
|
|
516
|
+
# Calculate cumulative counts
|
|
517
|
+
cumulative = []
|
|
518
|
+
running = 0
|
|
519
|
+
for c in bucket_counts:
|
|
520
|
+
running += c
|
|
521
|
+
cumulative.append(running)
|
|
522
|
+
|
|
523
|
+
data = {
|
|
524
|
+
"buckets": {
|
|
525
|
+
str(bound): cum
|
|
526
|
+
for bound, cum in zip(self._buckets, cumulative)
|
|
527
|
+
},
|
|
528
|
+
"sum": total_sum,
|
|
529
|
+
"count": count,
|
|
530
|
+
}
|
|
531
|
+
results.append((dict(key.labels), data))
|
|
532
|
+
return results
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
class LabeledHistogram:
|
|
536
|
+
"""Histogram with pre-set labels."""
|
|
537
|
+
|
|
538
|
+
def __init__(self, histogram: Histogram, labels: dict[str, str]) -> None:
|
|
539
|
+
self._histogram = histogram
|
|
540
|
+
self._labels = labels
|
|
541
|
+
|
|
542
|
+
def observe(self, value: float) -> None:
|
|
543
|
+
"""Observe a value."""
|
|
544
|
+
self._histogram.observe(value, **self._labels)
|
|
545
|
+
|
|
546
|
+
@contextmanager
|
|
547
|
+
def time(self) -> Iterator[None]:
|
|
548
|
+
"""Time a block of code."""
|
|
549
|
+
with self._histogram.time(**self._labels):
|
|
550
|
+
yield
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
# =============================================================================
|
|
554
|
+
# Summary
|
|
555
|
+
# =============================================================================
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
class Summary(Metric):
|
|
559
|
+
"""Summary with streaming quantiles.
|
|
560
|
+
|
|
561
|
+
Similar to Histogram but calculates quantiles on the client side.
|
|
562
|
+
Use when you need specific percentiles (p50, p90, p99).
|
|
563
|
+
|
|
564
|
+
Note: This is a simplified implementation that keeps recent samples.
|
|
565
|
+
For production use, consider a proper streaming quantile algorithm.
|
|
566
|
+
"""
|
|
567
|
+
|
|
568
|
+
def __init__(
|
|
569
|
+
self,
|
|
570
|
+
name: str,
|
|
571
|
+
description: str = "",
|
|
572
|
+
*,
|
|
573
|
+
quantiles: tuple[float, ...] = (0.5, 0.9, 0.99),
|
|
574
|
+
max_samples: int = 1000,
|
|
575
|
+
labels: tuple[str, ...] | list[str] = (),
|
|
576
|
+
**kwargs: Any,
|
|
577
|
+
) -> None:
|
|
578
|
+
"""Initialize summary.
|
|
579
|
+
|
|
580
|
+
Args:
|
|
581
|
+
name: Summary name.
|
|
582
|
+
description: Human-readable description.
|
|
583
|
+
quantiles: Quantiles to calculate (0.0-1.0).
|
|
584
|
+
max_samples: Maximum samples to keep.
|
|
585
|
+
labels: Label names.
|
|
586
|
+
**kwargs: Additional arguments.
|
|
587
|
+
"""
|
|
588
|
+
super().__init__(name, description, label_names=labels, **kwargs)
|
|
589
|
+
self._quantiles = quantiles
|
|
590
|
+
self._max_samples = max_samples
|
|
591
|
+
self._data: dict[MetricKey, tuple[list[float], float, int]] = {}
|
|
592
|
+
|
|
593
|
+
@property
|
|
594
|
+
def type(self) -> MetricType:
|
|
595
|
+
return MetricType.SUMMARY
|
|
596
|
+
|
|
597
|
+
def observe(self, value: float, **labels: str) -> None:
|
|
598
|
+
"""Observe a value.
|
|
599
|
+
|
|
600
|
+
Args:
|
|
601
|
+
value: Observed value.
|
|
602
|
+
**labels: Label values.
|
|
603
|
+
"""
|
|
604
|
+
if self._label_names:
|
|
605
|
+
self._validate_labels(labels)
|
|
606
|
+
|
|
607
|
+
key = MetricKey.create(self._name, labels)
|
|
608
|
+
|
|
609
|
+
with self._lock:
|
|
610
|
+
if key not in self._data:
|
|
611
|
+
self._data[key] = ([], 0.0, 0)
|
|
612
|
+
|
|
613
|
+
samples, total_sum, count = self._data[key]
|
|
614
|
+
|
|
615
|
+
# Add sample (circular buffer behavior)
|
|
616
|
+
if len(samples) >= self._max_samples:
|
|
617
|
+
samples.pop(0)
|
|
618
|
+
samples.append(value)
|
|
619
|
+
|
|
620
|
+
self._data[key] = (samples, total_sum + value, count + 1)
|
|
621
|
+
|
|
622
|
+
@contextmanager
|
|
623
|
+
def time(self, **labels: str) -> Iterator[None]:
|
|
624
|
+
"""Time a block of code."""
|
|
625
|
+
start = time.perf_counter()
|
|
626
|
+
try:
|
|
627
|
+
yield
|
|
628
|
+
finally:
|
|
629
|
+
self.observe(time.perf_counter() - start, **labels)
|
|
630
|
+
|
|
631
|
+
def labels(self, **labels: str) -> "LabeledSummary":
|
|
632
|
+
"""Get summary with specific labels."""
|
|
633
|
+
return LabeledSummary(self, labels)
|
|
634
|
+
|
|
635
|
+
def collect(self) -> list[tuple[dict[str, str], dict[str, Any]]]:
|
|
636
|
+
"""Collect summary data with quantiles."""
|
|
637
|
+
with self._lock:
|
|
638
|
+
results = []
|
|
639
|
+
for key, (samples, total_sum, count) in self._data.items():
|
|
640
|
+
quantile_values = {}
|
|
641
|
+
if samples:
|
|
642
|
+
sorted_samples = sorted(samples)
|
|
643
|
+
n = len(sorted_samples)
|
|
644
|
+
for q in self._quantiles:
|
|
645
|
+
idx = int(q * (n - 1))
|
|
646
|
+
quantile_values[f"p{int(q*100)}"] = sorted_samples[idx]
|
|
647
|
+
|
|
648
|
+
data = {
|
|
649
|
+
"quantiles": quantile_values,
|
|
650
|
+
"sum": total_sum,
|
|
651
|
+
"count": count,
|
|
652
|
+
}
|
|
653
|
+
results.append((dict(key.labels), data))
|
|
654
|
+
return results
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
class LabeledSummary:
|
|
658
|
+
"""Summary with pre-set labels."""
|
|
659
|
+
|
|
660
|
+
def __init__(self, summary: Summary, labels: dict[str, str]) -> None:
|
|
661
|
+
self._summary = summary
|
|
662
|
+
self._labels = labels
|
|
663
|
+
|
|
664
|
+
def observe(self, value: float) -> None:
|
|
665
|
+
"""Observe a value."""
|
|
666
|
+
self._summary.observe(value, **self._labels)
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
# =============================================================================
|
|
670
|
+
# Metrics Registry
|
|
671
|
+
# =============================================================================
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
class MetricsRegistry:
|
|
675
|
+
"""Central registry for all metrics.
|
|
676
|
+
|
|
677
|
+
Ensures unique metric names and provides collection.
|
|
678
|
+
"""
|
|
679
|
+
|
|
680
|
+
def __init__(self) -> None:
|
|
681
|
+
"""Initialize registry."""
|
|
682
|
+
self._metrics: dict[str, Metric] = {}
|
|
683
|
+
self._lock = threading.Lock()
|
|
684
|
+
|
|
685
|
+
def register(self, metric: Metric) -> Metric:
|
|
686
|
+
"""Register a metric.
|
|
687
|
+
|
|
688
|
+
Args:
|
|
689
|
+
metric: Metric to register.
|
|
690
|
+
|
|
691
|
+
Returns:
|
|
692
|
+
The registered metric.
|
|
693
|
+
|
|
694
|
+
Raises:
|
|
695
|
+
ValueError: If metric name already registered with different type.
|
|
696
|
+
"""
|
|
697
|
+
with self._lock:
|
|
698
|
+
if metric.name in self._metrics:
|
|
699
|
+
existing = self._metrics[metric.name]
|
|
700
|
+
if existing.type != metric.type:
|
|
701
|
+
raise ValueError(
|
|
702
|
+
f"Metric '{metric.name}' already registered "
|
|
703
|
+
f"as {existing.type.value}"
|
|
704
|
+
)
|
|
705
|
+
return existing
|
|
706
|
+
self._metrics[metric.name] = metric
|
|
707
|
+
return metric
|
|
708
|
+
|
|
709
|
+
def get(self, name: str) -> Metric | None:
|
|
710
|
+
"""Get a registered metric by name."""
|
|
711
|
+
return self._metrics.get(name)
|
|
712
|
+
|
|
713
|
+
def unregister(self, name: str) -> bool:
|
|
714
|
+
"""Unregister a metric.
|
|
715
|
+
|
|
716
|
+
Args:
|
|
717
|
+
name: Metric name.
|
|
718
|
+
|
|
719
|
+
Returns:
|
|
720
|
+
True if unregistered, False if not found.
|
|
721
|
+
"""
|
|
722
|
+
with self._lock:
|
|
723
|
+
if name in self._metrics:
|
|
724
|
+
del self._metrics[name]
|
|
725
|
+
return True
|
|
726
|
+
return False
|
|
727
|
+
|
|
728
|
+
def collect_all(self) -> dict[str, Any]:
|
|
729
|
+
"""Collect all metrics.
|
|
730
|
+
|
|
731
|
+
Returns:
|
|
732
|
+
Dictionary mapping metric names to their collected data.
|
|
733
|
+
"""
|
|
734
|
+
with self._lock:
|
|
735
|
+
result = {}
|
|
736
|
+
for name, metric in self._metrics.items():
|
|
737
|
+
result[name] = {
|
|
738
|
+
"type": metric.type.value,
|
|
739
|
+
"description": metric.description,
|
|
740
|
+
"data": metric.collect(),
|
|
741
|
+
}
|
|
742
|
+
return result
|
|
743
|
+
|
|
744
|
+
def clear(self) -> None:
|
|
745
|
+
"""Clear all registered metrics."""
|
|
746
|
+
with self._lock:
|
|
747
|
+
self._metrics.clear()
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
# =============================================================================
|
|
751
|
+
# Metrics Collector
|
|
752
|
+
# =============================================================================
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
class MetricsCollector:
|
|
756
|
+
"""High-level interface for creating and managing metrics.
|
|
757
|
+
|
|
758
|
+
MetricsCollector provides a fluent API for creating metrics and
|
|
759
|
+
manages their registration in the registry.
|
|
760
|
+
|
|
761
|
+
Example:
|
|
762
|
+
>>> collector = MetricsCollector()
|
|
763
|
+
>>> requests = collector.counter(
|
|
764
|
+
... "http_requests_total",
|
|
765
|
+
... "Total HTTP requests",
|
|
766
|
+
... labels=["method", "status"],
|
|
767
|
+
... )
|
|
768
|
+
>>> latency = collector.histogram(
|
|
769
|
+
... "request_duration_seconds",
|
|
770
|
+
... "Request latency",
|
|
771
|
+
... )
|
|
772
|
+
"""
|
|
773
|
+
|
|
774
|
+
def __init__(self, registry: MetricsRegistry | None = None) -> None:
|
|
775
|
+
"""Initialize collector.
|
|
776
|
+
|
|
777
|
+
Args:
|
|
778
|
+
registry: Metrics registry (creates new if None).
|
|
779
|
+
"""
|
|
780
|
+
self._registry = registry or MetricsRegistry()
|
|
781
|
+
|
|
782
|
+
@property
|
|
783
|
+
def registry(self) -> MetricsRegistry:
|
|
784
|
+
"""Get the metrics registry."""
|
|
785
|
+
return self._registry
|
|
786
|
+
|
|
787
|
+
def counter(
|
|
788
|
+
self,
|
|
789
|
+
name: str,
|
|
790
|
+
description: str = "",
|
|
791
|
+
*,
|
|
792
|
+
labels: list[str] | tuple[str, ...] = (),
|
|
793
|
+
**kwargs: Any,
|
|
794
|
+
) -> Counter:
|
|
795
|
+
"""Create or get a counter.
|
|
796
|
+
|
|
797
|
+
Args:
|
|
798
|
+
name: Counter name.
|
|
799
|
+
description: Human-readable description.
|
|
800
|
+
labels: Label names.
|
|
801
|
+
**kwargs: Additional arguments.
|
|
802
|
+
|
|
803
|
+
Returns:
|
|
804
|
+
Counter instance.
|
|
805
|
+
"""
|
|
806
|
+
counter = Counter(name, description, labels=labels, **kwargs)
|
|
807
|
+
return self._registry.register(counter) # type: ignore
|
|
808
|
+
|
|
809
|
+
def gauge(
|
|
810
|
+
self,
|
|
811
|
+
name: str,
|
|
812
|
+
description: str = "",
|
|
813
|
+
*,
|
|
814
|
+
labels: list[str] | tuple[str, ...] = (),
|
|
815
|
+
**kwargs: Any,
|
|
816
|
+
) -> Gauge:
|
|
817
|
+
"""Create or get a gauge.
|
|
818
|
+
|
|
819
|
+
Args:
|
|
820
|
+
name: Gauge name.
|
|
821
|
+
description: Human-readable description.
|
|
822
|
+
labels: Label names.
|
|
823
|
+
**kwargs: Additional arguments.
|
|
824
|
+
|
|
825
|
+
Returns:
|
|
826
|
+
Gauge instance.
|
|
827
|
+
"""
|
|
828
|
+
gauge = Gauge(name, description, labels=labels, **kwargs)
|
|
829
|
+
return self._registry.register(gauge) # type: ignore
|
|
830
|
+
|
|
831
|
+
def histogram(
|
|
832
|
+
self,
|
|
833
|
+
name: str,
|
|
834
|
+
description: str = "",
|
|
835
|
+
*,
|
|
836
|
+
buckets: list[float] | tuple[float, ...] | None = None,
|
|
837
|
+
labels: list[str] | tuple[str, ...] = (),
|
|
838
|
+
**kwargs: Any,
|
|
839
|
+
) -> Histogram:
|
|
840
|
+
"""Create or get a histogram.
|
|
841
|
+
|
|
842
|
+
Args:
|
|
843
|
+
name: Histogram name.
|
|
844
|
+
description: Human-readable description.
|
|
845
|
+
buckets: Bucket boundaries.
|
|
846
|
+
labels: Label names.
|
|
847
|
+
**kwargs: Additional arguments.
|
|
848
|
+
|
|
849
|
+
Returns:
|
|
850
|
+
Histogram instance.
|
|
851
|
+
"""
|
|
852
|
+
histogram = Histogram(
|
|
853
|
+
name, description, buckets=buckets, labels=labels, **kwargs
|
|
854
|
+
)
|
|
855
|
+
return self._registry.register(histogram) # type: ignore
|
|
856
|
+
|
|
857
|
+
def summary(
|
|
858
|
+
self,
|
|
859
|
+
name: str,
|
|
860
|
+
description: str = "",
|
|
861
|
+
*,
|
|
862
|
+
quantiles: tuple[float, ...] = (0.5, 0.9, 0.99),
|
|
863
|
+
labels: list[str] | tuple[str, ...] = (),
|
|
864
|
+
**kwargs: Any,
|
|
865
|
+
) -> Summary:
|
|
866
|
+
"""Create or get a summary.
|
|
867
|
+
|
|
868
|
+
Args:
|
|
869
|
+
name: Summary name.
|
|
870
|
+
description: Human-readable description.
|
|
871
|
+
quantiles: Quantiles to calculate.
|
|
872
|
+
labels: Label names.
|
|
873
|
+
**kwargs: Additional arguments.
|
|
874
|
+
|
|
875
|
+
Returns:
|
|
876
|
+
Summary instance.
|
|
877
|
+
"""
|
|
878
|
+
summary = Summary(
|
|
879
|
+
name, description, quantiles=quantiles, labels=labels, **kwargs
|
|
880
|
+
)
|
|
881
|
+
return self._registry.register(summary) # type: ignore
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
# =============================================================================
|
|
885
|
+
# Metrics Exporters
|
|
886
|
+
# =============================================================================
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
class MetricsExporter(ABC):
|
|
890
|
+
"""Abstract base class for metrics exporters."""
|
|
891
|
+
|
|
892
|
+
@abstractmethod
|
|
893
|
+
def export(self, registry: MetricsRegistry) -> str:
|
|
894
|
+
"""Export metrics from registry.
|
|
895
|
+
|
|
896
|
+
Args:
|
|
897
|
+
registry: Metrics registry to export.
|
|
898
|
+
|
|
899
|
+
Returns:
|
|
900
|
+
Exported metrics as string.
|
|
901
|
+
"""
|
|
902
|
+
pass
|
|
903
|
+
|
|
904
|
+
@abstractmethod
|
|
905
|
+
def push(self, registry: MetricsRegistry, endpoint: str) -> bool:
|
|
906
|
+
"""Push metrics to remote endpoint.
|
|
907
|
+
|
|
908
|
+
Args:
|
|
909
|
+
registry: Metrics registry to push.
|
|
910
|
+
endpoint: Remote endpoint URL.
|
|
911
|
+
|
|
912
|
+
Returns:
|
|
913
|
+
True if successful.
|
|
914
|
+
"""
|
|
915
|
+
pass
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
class PrometheusExporter(MetricsExporter):
|
|
919
|
+
"""Export metrics in Prometheus text format.
|
|
920
|
+
|
|
921
|
+
Generates output compatible with Prometheus text exposition format.
|
|
922
|
+
|
|
923
|
+
Example output:
|
|
924
|
+
# HELP http_requests_total Total HTTP requests
|
|
925
|
+
# TYPE http_requests_total counter
|
|
926
|
+
http_requests_total{method="GET",status="200"} 1234
|
|
927
|
+
"""
|
|
928
|
+
|
|
929
|
+
def export(self, registry: MetricsRegistry) -> str:
|
|
930
|
+
"""Export metrics in Prometheus format."""
|
|
931
|
+
lines = []
|
|
932
|
+
data = registry.collect_all()
|
|
933
|
+
|
|
934
|
+
for name, info in data.items():
|
|
935
|
+
metric_type = info["type"]
|
|
936
|
+
description = info["description"]
|
|
937
|
+
metric_data = info["data"]
|
|
938
|
+
|
|
939
|
+
# HELP and TYPE lines
|
|
940
|
+
lines.append(f"# HELP {name} {description}")
|
|
941
|
+
lines.append(f"# TYPE {name} {metric_type}")
|
|
942
|
+
|
|
943
|
+
# Metric lines
|
|
944
|
+
for labels, value in metric_data:
|
|
945
|
+
label_str = self._format_labels(labels)
|
|
946
|
+
|
|
947
|
+
if metric_type == "histogram":
|
|
948
|
+
# Histogram has special format
|
|
949
|
+
for bound, count in value["buckets"].items():
|
|
950
|
+
bound_label = f'{label_str},le="{bound}"' if label_str else f'le="{bound}"'
|
|
951
|
+
lines.append(f"{name}_bucket{{{bound_label}}} {count}")
|
|
952
|
+
sum_labels = f"{{{label_str}}}" if label_str else ""
|
|
953
|
+
lines.append(f"{name}_sum{sum_labels} {value['sum']}")
|
|
954
|
+
lines.append(f"{name}_count{sum_labels} {value['count']}")
|
|
955
|
+
|
|
956
|
+
elif metric_type == "summary":
|
|
957
|
+
for quantile_name, quantile_value in value.get("quantiles", {}).items():
|
|
958
|
+
q = quantile_name[1:] # Remove 'p' prefix
|
|
959
|
+
q_label = f'{label_str},quantile="{int(q)/100}"' if label_str else f'quantile="{int(q)/100}"'
|
|
960
|
+
lines.append(f"{name}{{{q_label}}} {quantile_value}")
|
|
961
|
+
sum_labels = f"{{{label_str}}}" if label_str else ""
|
|
962
|
+
lines.append(f"{name}_sum{sum_labels} {value['sum']}")
|
|
963
|
+
lines.append(f"{name}_count{sum_labels} {value['count']}")
|
|
964
|
+
|
|
965
|
+
else:
|
|
966
|
+
# Counter and Gauge
|
|
967
|
+
if label_str:
|
|
968
|
+
lines.append(f"{name}{{{label_str}}} {value}")
|
|
969
|
+
else:
|
|
970
|
+
lines.append(f"{name} {value}")
|
|
971
|
+
|
|
972
|
+
lines.append("") # Empty line between metrics
|
|
973
|
+
|
|
974
|
+
return "\n".join(lines)
|
|
975
|
+
|
|
976
|
+
def _format_labels(self, labels: dict[str, str]) -> str:
|
|
977
|
+
"""Format labels for Prometheus."""
|
|
978
|
+
if not labels:
|
|
979
|
+
return ""
|
|
980
|
+
parts = [f'{k}="{v}"' for k, v in sorted(labels.items())]
|
|
981
|
+
return ",".join(parts)
|
|
982
|
+
|
|
983
|
+
def push(self, registry: MetricsRegistry, endpoint: str) -> bool:
|
|
984
|
+
"""Push metrics to Prometheus Pushgateway."""
|
|
985
|
+
import urllib.request
|
|
986
|
+
import urllib.error
|
|
987
|
+
|
|
988
|
+
data = self.export(registry).encode("utf-8")
|
|
989
|
+
|
|
990
|
+
try:
|
|
991
|
+
request = urllib.request.Request(
|
|
992
|
+
endpoint,
|
|
993
|
+
data=data,
|
|
994
|
+
method="POST",
|
|
995
|
+
headers={"Content-Type": "text/plain"},
|
|
996
|
+
)
|
|
997
|
+
with urllib.request.urlopen(request, timeout=30):
|
|
998
|
+
return True
|
|
999
|
+
except urllib.error.URLError:
|
|
1000
|
+
return False
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
class StatsDExporter(MetricsExporter):
|
|
1004
|
+
"""Export metrics in StatsD format.
|
|
1005
|
+
|
|
1006
|
+
Sends metrics via UDP to a StatsD server.
|
|
1007
|
+
"""
|
|
1008
|
+
|
|
1009
|
+
def __init__(
|
|
1010
|
+
self,
|
|
1011
|
+
host: str = "localhost",
|
|
1012
|
+
port: int = 8125,
|
|
1013
|
+
prefix: str = "",
|
|
1014
|
+
) -> None:
|
|
1015
|
+
"""Initialize StatsD exporter.
|
|
1016
|
+
|
|
1017
|
+
Args:
|
|
1018
|
+
host: StatsD server host.
|
|
1019
|
+
port: StatsD server port.
|
|
1020
|
+
prefix: Metric name prefix.
|
|
1021
|
+
"""
|
|
1022
|
+
self._host = host
|
|
1023
|
+
self._port = port
|
|
1024
|
+
self._prefix = prefix
|
|
1025
|
+
|
|
1026
|
+
def export(self, registry: MetricsRegistry) -> str:
|
|
1027
|
+
"""Export metrics in StatsD format."""
|
|
1028
|
+
lines = []
|
|
1029
|
+
data = registry.collect_all()
|
|
1030
|
+
|
|
1031
|
+
for name, info in data.items():
|
|
1032
|
+
metric_type = info["type"]
|
|
1033
|
+
metric_data = info["data"]
|
|
1034
|
+
|
|
1035
|
+
full_name = f"{self._prefix}{name}" if self._prefix else name
|
|
1036
|
+
|
|
1037
|
+
for labels, value in metric_data:
|
|
1038
|
+
label_suffix = self._format_labels(labels)
|
|
1039
|
+
metric_name = f"{full_name}{label_suffix}"
|
|
1040
|
+
|
|
1041
|
+
if metric_type == "counter":
|
|
1042
|
+
lines.append(f"{metric_name}:{value}|c")
|
|
1043
|
+
elif metric_type == "gauge":
|
|
1044
|
+
lines.append(f"{metric_name}:{value}|g")
|
|
1045
|
+
elif metric_type == "histogram":
|
|
1046
|
+
# Send as timing
|
|
1047
|
+
lines.append(f"{metric_name}:{value['sum']/max(value['count'], 1)*1000}|ms")
|
|
1048
|
+
elif metric_type == "summary":
|
|
1049
|
+
lines.append(f"{metric_name}:{value['sum']/max(value['count'], 1)*1000}|ms")
|
|
1050
|
+
|
|
1051
|
+
return "\n".join(lines)
|
|
1052
|
+
|
|
1053
|
+
def _format_labels(self, labels: dict[str, str]) -> str:
|
|
1054
|
+
"""Format labels as suffix."""
|
|
1055
|
+
if not labels:
|
|
1056
|
+
return ""
|
|
1057
|
+
parts = [f".{k}_{v}" for k, v in sorted(labels.items())]
|
|
1058
|
+
return "".join(parts)
|
|
1059
|
+
|
|
1060
|
+
def push(self, registry: MetricsRegistry, endpoint: str | None = None) -> bool:
|
|
1061
|
+
"""Send metrics to StatsD server via UDP."""
|
|
1062
|
+
import socket
|
|
1063
|
+
|
|
1064
|
+
data = self.export(registry)
|
|
1065
|
+
|
|
1066
|
+
try:
|
|
1067
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
1068
|
+
sock.sendto(data.encode("utf-8"), (self._host, self._port))
|
|
1069
|
+
sock.close()
|
|
1070
|
+
return True
|
|
1071
|
+
except socket.error:
|
|
1072
|
+
return False
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
class InMemoryExporter(MetricsExporter):
|
|
1076
|
+
"""In-memory exporter for testing.
|
|
1077
|
+
|
|
1078
|
+
Stores exported metrics for inspection.
|
|
1079
|
+
"""
|
|
1080
|
+
|
|
1081
|
+
def __init__(self) -> None:
|
|
1082
|
+
"""Initialize in-memory exporter."""
|
|
1083
|
+
self._exports: list[dict[str, Any]] = []
|
|
1084
|
+
|
|
1085
|
+
@property
|
|
1086
|
+
def exports(self) -> list[dict[str, Any]]:
|
|
1087
|
+
"""Get all exports."""
|
|
1088
|
+
return self._exports
|
|
1089
|
+
|
|
1090
|
+
def export(self, registry: MetricsRegistry) -> str:
|
|
1091
|
+
"""Export metrics to memory."""
|
|
1092
|
+
import json
|
|
1093
|
+
data = registry.collect_all()
|
|
1094
|
+
self._exports.append(data)
|
|
1095
|
+
return json.dumps(data, default=str)
|
|
1096
|
+
|
|
1097
|
+
def push(self, registry: MetricsRegistry, endpoint: str) -> bool:
|
|
1098
|
+
"""Push to memory (always succeeds)."""
|
|
1099
|
+
self.export(registry)
|
|
1100
|
+
return True
|
|
1101
|
+
|
|
1102
|
+
def clear(self) -> None:
|
|
1103
|
+
"""Clear stored exports."""
|
|
1104
|
+
self._exports.clear()
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
# =============================================================================
|
|
1108
|
+
# Global Metrics
|
|
1109
|
+
# =============================================================================
|
|
1110
|
+
|
|
1111
|
+
_global_collector: MetricsCollector | None = None
|
|
1112
|
+
_lock = threading.Lock()
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
def get_metrics() -> MetricsCollector:
|
|
1116
|
+
"""Get the global metrics collector.
|
|
1117
|
+
|
|
1118
|
+
Returns:
|
|
1119
|
+
Global MetricsCollector instance.
|
|
1120
|
+
"""
|
|
1121
|
+
global _global_collector
|
|
1122
|
+
|
|
1123
|
+
with _lock:
|
|
1124
|
+
if _global_collector is None:
|
|
1125
|
+
_global_collector = MetricsCollector()
|
|
1126
|
+
return _global_collector
|
|
1127
|
+
|
|
1128
|
+
|
|
1129
|
+
def set_metrics(collector: MetricsCollector) -> None:
|
|
1130
|
+
"""Set the global metrics collector.
|
|
1131
|
+
|
|
1132
|
+
Args:
|
|
1133
|
+
collector: MetricsCollector to use globally.
|
|
1134
|
+
"""
|
|
1135
|
+
global _global_collector
|
|
1136
|
+
|
|
1137
|
+
with _lock:
|
|
1138
|
+
_global_collector = collector
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
def configure_metrics(
|
|
1142
|
+
*,
|
|
1143
|
+
exporter: MetricsExporter | None = None,
|
|
1144
|
+
registry: MetricsRegistry | None = None,
|
|
1145
|
+
) -> MetricsCollector:
|
|
1146
|
+
"""Configure global metrics.
|
|
1147
|
+
|
|
1148
|
+
Args:
|
|
1149
|
+
exporter: Exporter to use.
|
|
1150
|
+
registry: Registry to use.
|
|
1151
|
+
|
|
1152
|
+
Returns:
|
|
1153
|
+
Configured MetricsCollector.
|
|
1154
|
+
"""
|
|
1155
|
+
collector = MetricsCollector(registry=registry)
|
|
1156
|
+
set_metrics(collector)
|
|
1157
|
+
return collector
|