oscura 0.0.1__py3-none-any.whl → 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oscura/__init__.py +813 -8
- oscura/__main__.py +392 -0
- oscura/analyzers/__init__.py +37 -0
- oscura/analyzers/digital/__init__.py +177 -0
- oscura/analyzers/digital/bus.py +691 -0
- oscura/analyzers/digital/clock.py +805 -0
- oscura/analyzers/digital/correlation.py +720 -0
- oscura/analyzers/digital/edges.py +632 -0
- oscura/analyzers/digital/extraction.py +413 -0
- oscura/analyzers/digital/quality.py +878 -0
- oscura/analyzers/digital/signal_quality.py +877 -0
- oscura/analyzers/digital/thresholds.py +708 -0
- oscura/analyzers/digital/timing.py +1104 -0
- oscura/analyzers/eye/__init__.py +46 -0
- oscura/analyzers/eye/diagram.py +434 -0
- oscura/analyzers/eye/metrics.py +555 -0
- oscura/analyzers/jitter/__init__.py +83 -0
- oscura/analyzers/jitter/ber.py +333 -0
- oscura/analyzers/jitter/decomposition.py +759 -0
- oscura/analyzers/jitter/measurements.py +413 -0
- oscura/analyzers/jitter/spectrum.py +220 -0
- oscura/analyzers/measurements.py +40 -0
- oscura/analyzers/packet/__init__.py +171 -0
- oscura/analyzers/packet/daq.py +1077 -0
- oscura/analyzers/packet/metrics.py +437 -0
- oscura/analyzers/packet/parser.py +327 -0
- oscura/analyzers/packet/payload.py +2156 -0
- oscura/analyzers/packet/payload_analysis.py +1312 -0
- oscura/analyzers/packet/payload_extraction.py +236 -0
- oscura/analyzers/packet/payload_patterns.py +670 -0
- oscura/analyzers/packet/stream.py +359 -0
- oscura/analyzers/patterns/__init__.py +266 -0
- oscura/analyzers/patterns/clustering.py +1036 -0
- oscura/analyzers/patterns/discovery.py +539 -0
- oscura/analyzers/patterns/learning.py +797 -0
- oscura/analyzers/patterns/matching.py +1091 -0
- oscura/analyzers/patterns/periodic.py +650 -0
- oscura/analyzers/patterns/sequences.py +767 -0
- oscura/analyzers/power/__init__.py +116 -0
- oscura/analyzers/power/ac_power.py +391 -0
- oscura/analyzers/power/basic.py +383 -0
- oscura/analyzers/power/conduction.py +314 -0
- oscura/analyzers/power/efficiency.py +297 -0
- oscura/analyzers/power/ripple.py +356 -0
- oscura/analyzers/power/soa.py +372 -0
- oscura/analyzers/power/switching.py +479 -0
- oscura/analyzers/protocol/__init__.py +150 -0
- oscura/analyzers/protocols/__init__.py +150 -0
- oscura/analyzers/protocols/base.py +500 -0
- oscura/analyzers/protocols/can.py +620 -0
- oscura/analyzers/protocols/can_fd.py +448 -0
- oscura/analyzers/protocols/flexray.py +405 -0
- oscura/analyzers/protocols/hdlc.py +399 -0
- oscura/analyzers/protocols/i2c.py +368 -0
- oscura/analyzers/protocols/i2s.py +296 -0
- oscura/analyzers/protocols/jtag.py +393 -0
- oscura/analyzers/protocols/lin.py +445 -0
- oscura/analyzers/protocols/manchester.py +333 -0
- oscura/analyzers/protocols/onewire.py +501 -0
- oscura/analyzers/protocols/spi.py +334 -0
- oscura/analyzers/protocols/swd.py +325 -0
- oscura/analyzers/protocols/uart.py +393 -0
- oscura/analyzers/protocols/usb.py +495 -0
- oscura/analyzers/signal_integrity/__init__.py +63 -0
- oscura/analyzers/signal_integrity/embedding.py +294 -0
- oscura/analyzers/signal_integrity/equalization.py +370 -0
- oscura/analyzers/signal_integrity/sparams.py +484 -0
- oscura/analyzers/spectral/__init__.py +53 -0
- oscura/analyzers/spectral/chunked.py +273 -0
- oscura/analyzers/spectral/chunked_fft.py +571 -0
- oscura/analyzers/spectral/chunked_wavelet.py +391 -0
- oscura/analyzers/spectral/fft.py +92 -0
- oscura/analyzers/statistical/__init__.py +250 -0
- oscura/analyzers/statistical/checksum.py +923 -0
- oscura/analyzers/statistical/chunked_corr.py +228 -0
- oscura/analyzers/statistical/classification.py +778 -0
- oscura/analyzers/statistical/entropy.py +1113 -0
- oscura/analyzers/statistical/ngrams.py +614 -0
- oscura/analyzers/statistics/__init__.py +119 -0
- oscura/analyzers/statistics/advanced.py +885 -0
- oscura/analyzers/statistics/basic.py +263 -0
- oscura/analyzers/statistics/correlation.py +630 -0
- oscura/analyzers/statistics/distribution.py +298 -0
- oscura/analyzers/statistics/outliers.py +463 -0
- oscura/analyzers/statistics/streaming.py +93 -0
- oscura/analyzers/statistics/trend.py +520 -0
- oscura/analyzers/validation.py +598 -0
- oscura/analyzers/waveform/__init__.py +36 -0
- oscura/analyzers/waveform/measurements.py +943 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +371 -0
- oscura/analyzers/waveform/spectral.py +1689 -0
- oscura/analyzers/waveform/wavelets.py +298 -0
- oscura/api/__init__.py +62 -0
- oscura/api/dsl.py +538 -0
- oscura/api/fluent.py +571 -0
- oscura/api/operators.py +498 -0
- oscura/api/optimization.py +392 -0
- oscura/api/profiling.py +396 -0
- oscura/automotive/__init__.py +73 -0
- oscura/automotive/can/__init__.py +52 -0
- oscura/automotive/can/analysis.py +356 -0
- oscura/automotive/can/checksum.py +250 -0
- oscura/automotive/can/correlation.py +212 -0
- oscura/automotive/can/discovery.py +355 -0
- oscura/automotive/can/message_wrapper.py +375 -0
- oscura/automotive/can/models.py +385 -0
- oscura/automotive/can/patterns.py +381 -0
- oscura/automotive/can/session.py +452 -0
- oscura/automotive/can/state_machine.py +300 -0
- oscura/automotive/can/stimulus_response.py +461 -0
- oscura/automotive/dbc/__init__.py +15 -0
- oscura/automotive/dbc/generator.py +156 -0
- oscura/automotive/dbc/parser.py +146 -0
- oscura/automotive/dtc/__init__.py +30 -0
- oscura/automotive/dtc/database.py +3036 -0
- oscura/automotive/j1939/__init__.py +14 -0
- oscura/automotive/j1939/decoder.py +745 -0
- oscura/automotive/loaders/__init__.py +35 -0
- oscura/automotive/loaders/asc.py +98 -0
- oscura/automotive/loaders/blf.py +77 -0
- oscura/automotive/loaders/csv_can.py +136 -0
- oscura/automotive/loaders/dispatcher.py +136 -0
- oscura/automotive/loaders/mdf.py +331 -0
- oscura/automotive/loaders/pcap.py +132 -0
- oscura/automotive/obd/__init__.py +14 -0
- oscura/automotive/obd/decoder.py +707 -0
- oscura/automotive/uds/__init__.py +48 -0
- oscura/automotive/uds/decoder.py +265 -0
- oscura/automotive/uds/models.py +64 -0
- oscura/automotive/visualization.py +369 -0
- oscura/batch/__init__.py +55 -0
- oscura/batch/advanced.py +627 -0
- oscura/batch/aggregate.py +300 -0
- oscura/batch/analyze.py +139 -0
- oscura/batch/logging.py +487 -0
- oscura/batch/metrics.py +556 -0
- oscura/builders/__init__.py +41 -0
- oscura/builders/signal_builder.py +1131 -0
- oscura/cli/__init__.py +14 -0
- oscura/cli/batch.py +339 -0
- oscura/cli/characterize.py +273 -0
- oscura/cli/compare.py +775 -0
- oscura/cli/decode.py +551 -0
- oscura/cli/main.py +247 -0
- oscura/cli/shell.py +350 -0
- oscura/comparison/__init__.py +66 -0
- oscura/comparison/compare.py +397 -0
- oscura/comparison/golden.py +487 -0
- oscura/comparison/limits.py +391 -0
- oscura/comparison/mask.py +434 -0
- oscura/comparison/trace_diff.py +30 -0
- oscura/comparison/visualization.py +481 -0
- oscura/compliance/__init__.py +70 -0
- oscura/compliance/advanced.py +756 -0
- oscura/compliance/masks.py +363 -0
- oscura/compliance/reporting.py +483 -0
- oscura/compliance/testing.py +298 -0
- oscura/component/__init__.py +38 -0
- oscura/component/impedance.py +365 -0
- oscura/component/reactive.py +598 -0
- oscura/component/transmission_line.py +312 -0
- oscura/config/__init__.py +191 -0
- oscura/config/defaults.py +254 -0
- oscura/config/loader.py +348 -0
- oscura/config/memory.py +271 -0
- oscura/config/migration.py +458 -0
- oscura/config/pipeline.py +1077 -0
- oscura/config/preferences.py +530 -0
- oscura/config/protocol.py +875 -0
- oscura/config/schema.py +713 -0
- oscura/config/settings.py +420 -0
- oscura/config/thresholds.py +599 -0
- oscura/convenience.py +457 -0
- oscura/core/__init__.py +299 -0
- oscura/core/audit.py +457 -0
- oscura/core/backend_selector.py +405 -0
- oscura/core/cache.py +590 -0
- oscura/core/cancellation.py +439 -0
- oscura/core/confidence.py +225 -0
- oscura/core/config.py +506 -0
- oscura/core/correlation.py +216 -0
- oscura/core/cross_domain.py +422 -0
- oscura/core/debug.py +301 -0
- oscura/core/edge_cases.py +541 -0
- oscura/core/exceptions.py +535 -0
- oscura/core/gpu_backend.py +523 -0
- oscura/core/lazy.py +832 -0
- oscura/core/log_query.py +540 -0
- oscura/core/logging.py +931 -0
- oscura/core/logging_advanced.py +952 -0
- oscura/core/memoize.py +171 -0
- oscura/core/memory_check.py +274 -0
- oscura/core/memory_guard.py +290 -0
- oscura/core/memory_limits.py +336 -0
- oscura/core/memory_monitor.py +453 -0
- oscura/core/memory_progress.py +465 -0
- oscura/core/memory_warnings.py +315 -0
- oscura/core/numba_backend.py +362 -0
- oscura/core/performance.py +352 -0
- oscura/core/progress.py +524 -0
- oscura/core/provenance.py +358 -0
- oscura/core/results.py +331 -0
- oscura/core/types.py +504 -0
- oscura/core/uncertainty.py +383 -0
- oscura/discovery/__init__.py +52 -0
- oscura/discovery/anomaly_detector.py +672 -0
- oscura/discovery/auto_decoder.py +415 -0
- oscura/discovery/comparison.py +497 -0
- oscura/discovery/quality_validator.py +528 -0
- oscura/discovery/signal_detector.py +769 -0
- oscura/dsl/__init__.py +73 -0
- oscura/dsl/commands.py +246 -0
- oscura/dsl/interpreter.py +455 -0
- oscura/dsl/parser.py +689 -0
- oscura/dsl/repl.py +172 -0
- oscura/exceptions.py +59 -0
- oscura/exploratory/__init__.py +111 -0
- oscura/exploratory/error_recovery.py +642 -0
- oscura/exploratory/fuzzy.py +513 -0
- oscura/exploratory/fuzzy_advanced.py +786 -0
- oscura/exploratory/legacy.py +831 -0
- oscura/exploratory/parse.py +358 -0
- oscura/exploratory/recovery.py +275 -0
- oscura/exploratory/sync.py +382 -0
- oscura/exploratory/unknown.py +707 -0
- oscura/export/__init__.py +25 -0
- oscura/export/wireshark/README.md +265 -0
- oscura/export/wireshark/__init__.py +47 -0
- oscura/export/wireshark/generator.py +312 -0
- oscura/export/wireshark/lua_builder.py +159 -0
- oscura/export/wireshark/templates/dissector.lua.j2 +92 -0
- oscura/export/wireshark/type_mapping.py +165 -0
- oscura/export/wireshark/validator.py +105 -0
- oscura/exporters/__init__.py +94 -0
- oscura/exporters/csv.py +303 -0
- oscura/exporters/exporters.py +44 -0
- oscura/exporters/hdf5.py +219 -0
- oscura/exporters/html_export.py +701 -0
- oscura/exporters/json_export.py +291 -0
- oscura/exporters/markdown_export.py +367 -0
- oscura/exporters/matlab_export.py +354 -0
- oscura/exporters/npz_export.py +219 -0
- oscura/exporters/spice_export.py +210 -0
- oscura/extensibility/__init__.py +131 -0
- oscura/extensibility/docs.py +752 -0
- oscura/extensibility/extensions.py +1125 -0
- oscura/extensibility/logging.py +259 -0
- oscura/extensibility/measurements.py +485 -0
- oscura/extensibility/plugins.py +414 -0
- oscura/extensibility/registry.py +346 -0
- oscura/extensibility/templates.py +913 -0
- oscura/extensibility/validation.py +651 -0
- oscura/filtering/__init__.py +89 -0
- oscura/filtering/base.py +563 -0
- oscura/filtering/convenience.py +564 -0
- oscura/filtering/design.py +725 -0
- oscura/filtering/filters.py +32 -0
- oscura/filtering/introspection.py +605 -0
- oscura/guidance/__init__.py +24 -0
- oscura/guidance/recommender.py +429 -0
- oscura/guidance/wizard.py +518 -0
- oscura/inference/__init__.py +251 -0
- oscura/inference/active_learning/README.md +153 -0
- oscura/inference/active_learning/__init__.py +38 -0
- oscura/inference/active_learning/lstar.py +257 -0
- oscura/inference/active_learning/observation_table.py +230 -0
- oscura/inference/active_learning/oracle.py +78 -0
- oscura/inference/active_learning/teachers/__init__.py +15 -0
- oscura/inference/active_learning/teachers/simulator.py +192 -0
- oscura/inference/adaptive_tuning.py +453 -0
- oscura/inference/alignment.py +653 -0
- oscura/inference/bayesian.py +943 -0
- oscura/inference/binary.py +1016 -0
- oscura/inference/crc_reverse.py +711 -0
- oscura/inference/logic.py +288 -0
- oscura/inference/message_format.py +1305 -0
- oscura/inference/protocol.py +417 -0
- oscura/inference/protocol_dsl.py +1084 -0
- oscura/inference/protocol_library.py +1230 -0
- oscura/inference/sequences.py +809 -0
- oscura/inference/signal_intelligence.py +1509 -0
- oscura/inference/spectral.py +215 -0
- oscura/inference/state_machine.py +634 -0
- oscura/inference/stream.py +918 -0
- oscura/integrations/__init__.py +59 -0
- oscura/integrations/llm.py +1827 -0
- oscura/jupyter/__init__.py +32 -0
- oscura/jupyter/display.py +268 -0
- oscura/jupyter/magic.py +334 -0
- oscura/loaders/__init__.py +526 -0
- oscura/loaders/binary.py +69 -0
- oscura/loaders/configurable.py +1255 -0
- oscura/loaders/csv.py +26 -0
- oscura/loaders/csv_loader.py +473 -0
- oscura/loaders/hdf5.py +9 -0
- oscura/loaders/hdf5_loader.py +510 -0
- oscura/loaders/lazy.py +370 -0
- oscura/loaders/mmap_loader.py +583 -0
- oscura/loaders/numpy_loader.py +436 -0
- oscura/loaders/pcap.py +432 -0
- oscura/loaders/preprocessing.py +368 -0
- oscura/loaders/rigol.py +287 -0
- oscura/loaders/sigrok.py +321 -0
- oscura/loaders/tdms.py +367 -0
- oscura/loaders/tektronix.py +711 -0
- oscura/loaders/validation.py +584 -0
- oscura/loaders/vcd.py +464 -0
- oscura/loaders/wav.py +233 -0
- oscura/math/__init__.py +45 -0
- oscura/math/arithmetic.py +824 -0
- oscura/math/interpolation.py +413 -0
- oscura/onboarding/__init__.py +39 -0
- oscura/onboarding/help.py +498 -0
- oscura/onboarding/tutorials.py +405 -0
- oscura/onboarding/wizard.py +466 -0
- oscura/optimization/__init__.py +19 -0
- oscura/optimization/parallel.py +440 -0
- oscura/optimization/search.py +532 -0
- oscura/pipeline/__init__.py +43 -0
- oscura/pipeline/base.py +338 -0
- oscura/pipeline/composition.py +242 -0
- oscura/pipeline/parallel.py +448 -0
- oscura/pipeline/pipeline.py +375 -0
- oscura/pipeline/reverse_engineering.py +1119 -0
- oscura/plugins/__init__.py +122 -0
- oscura/plugins/base.py +272 -0
- oscura/plugins/cli.py +497 -0
- oscura/plugins/discovery.py +411 -0
- oscura/plugins/isolation.py +418 -0
- oscura/plugins/lifecycle.py +959 -0
- oscura/plugins/manager.py +493 -0
- oscura/plugins/registry.py +421 -0
- oscura/plugins/versioning.py +372 -0
- oscura/py.typed +0 -0
- oscura/quality/__init__.py +65 -0
- oscura/quality/ensemble.py +740 -0
- oscura/quality/explainer.py +338 -0
- oscura/quality/scoring.py +616 -0
- oscura/quality/warnings.py +456 -0
- oscura/reporting/__init__.py +248 -0
- oscura/reporting/advanced.py +1234 -0
- oscura/reporting/analyze.py +448 -0
- oscura/reporting/argument_preparer.py +596 -0
- oscura/reporting/auto_report.py +507 -0
- oscura/reporting/batch.py +615 -0
- oscura/reporting/chart_selection.py +223 -0
- oscura/reporting/comparison.py +330 -0
- oscura/reporting/config.py +615 -0
- oscura/reporting/content/__init__.py +39 -0
- oscura/reporting/content/executive.py +127 -0
- oscura/reporting/content/filtering.py +191 -0
- oscura/reporting/content/minimal.py +257 -0
- oscura/reporting/content/verbosity.py +162 -0
- oscura/reporting/core.py +508 -0
- oscura/reporting/core_formats/__init__.py +17 -0
- oscura/reporting/core_formats/multi_format.py +210 -0
- oscura/reporting/engine.py +836 -0
- oscura/reporting/export.py +366 -0
- oscura/reporting/formatting/__init__.py +129 -0
- oscura/reporting/formatting/emphasis.py +81 -0
- oscura/reporting/formatting/numbers.py +403 -0
- oscura/reporting/formatting/standards.py +55 -0
- oscura/reporting/formatting.py +466 -0
- oscura/reporting/html.py +578 -0
- oscura/reporting/index.py +590 -0
- oscura/reporting/multichannel.py +296 -0
- oscura/reporting/output.py +379 -0
- oscura/reporting/pdf.py +373 -0
- oscura/reporting/plots.py +731 -0
- oscura/reporting/pptx_export.py +360 -0
- oscura/reporting/renderers/__init__.py +11 -0
- oscura/reporting/renderers/pdf.py +94 -0
- oscura/reporting/sections.py +471 -0
- oscura/reporting/standards.py +680 -0
- oscura/reporting/summary_generator.py +368 -0
- oscura/reporting/tables.py +397 -0
- oscura/reporting/template_system.py +724 -0
- oscura/reporting/templates/__init__.py +15 -0
- oscura/reporting/templates/definition.py +205 -0
- oscura/reporting/templates/index.html +649 -0
- oscura/reporting/templates/index.md +173 -0
- oscura/schemas/__init__.py +158 -0
- oscura/schemas/bus_configuration.json +322 -0
- oscura/schemas/device_mapping.json +182 -0
- oscura/schemas/packet_format.json +418 -0
- oscura/schemas/protocol_definition.json +363 -0
- oscura/search/__init__.py +16 -0
- oscura/search/anomaly.py +292 -0
- oscura/search/context.py +149 -0
- oscura/search/pattern.py +160 -0
- oscura/session/__init__.py +34 -0
- oscura/session/annotations.py +289 -0
- oscura/session/history.py +313 -0
- oscura/session/session.py +445 -0
- oscura/streaming/__init__.py +43 -0
- oscura/streaming/chunked.py +611 -0
- oscura/streaming/progressive.py +393 -0
- oscura/streaming/realtime.py +622 -0
- oscura/testing/__init__.py +54 -0
- oscura/testing/synthetic.py +808 -0
- oscura/triggering/__init__.py +68 -0
- oscura/triggering/base.py +229 -0
- oscura/triggering/edge.py +353 -0
- oscura/triggering/pattern.py +344 -0
- oscura/triggering/pulse.py +581 -0
- oscura/triggering/window.py +453 -0
- oscura/ui/__init__.py +48 -0
- oscura/ui/formatters.py +526 -0
- oscura/ui/progressive_display.py +340 -0
- oscura/utils/__init__.py +99 -0
- oscura/utils/autodetect.py +338 -0
- oscura/utils/buffer.py +389 -0
- oscura/utils/lazy.py +407 -0
- oscura/utils/lazy_imports.py +147 -0
- oscura/utils/memory.py +836 -0
- oscura/utils/memory_advanced.py +1326 -0
- oscura/utils/memory_extensions.py +465 -0
- oscura/utils/progressive.py +352 -0
- oscura/utils/windowing.py +362 -0
- oscura/visualization/__init__.py +321 -0
- oscura/visualization/accessibility.py +526 -0
- oscura/visualization/annotations.py +374 -0
- oscura/visualization/axis_scaling.py +305 -0
- oscura/visualization/colors.py +453 -0
- oscura/visualization/digital.py +337 -0
- oscura/visualization/eye.py +420 -0
- oscura/visualization/histogram.py +281 -0
- oscura/visualization/interactive.py +858 -0
- oscura/visualization/jitter.py +702 -0
- oscura/visualization/keyboard.py +394 -0
- oscura/visualization/layout.py +365 -0
- oscura/visualization/optimization.py +1028 -0
- oscura/visualization/palettes.py +446 -0
- oscura/visualization/plot.py +92 -0
- oscura/visualization/power.py +290 -0
- oscura/visualization/power_extended.py +626 -0
- oscura/visualization/presets.py +467 -0
- oscura/visualization/protocols.py +932 -0
- oscura/visualization/render.py +207 -0
- oscura/visualization/rendering.py +444 -0
- oscura/visualization/reverse_engineering.py +791 -0
- oscura/visualization/signal_integrity.py +808 -0
- oscura/visualization/specialized.py +553 -0
- oscura/visualization/spectral.py +811 -0
- oscura/visualization/styles.py +381 -0
- oscura/visualization/thumbnails.py +311 -0
- oscura/visualization/time_axis.py +351 -0
- oscura/visualization/waveform.py +367 -0
- oscura/workflow/__init__.py +13 -0
- oscura/workflow/dag.py +377 -0
- oscura/workflows/__init__.py +58 -0
- oscura/workflows/compliance.py +280 -0
- oscura/workflows/digital.py +272 -0
- oscura/workflows/multi_trace.py +502 -0
- oscura/workflows/power.py +178 -0
- oscura/workflows/protocol.py +492 -0
- oscura/workflows/reverse_engineering.py +639 -0
- oscura/workflows/signal_integrity.py +227 -0
- oscura-0.1.0.dist-info/METADATA +300 -0
- oscura-0.1.0.dist-info/RECORD +463 -0
- oscura-0.1.0.dist-info/entry_points.txt +2 -0
- {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/licenses/LICENSE +1 -1
- oscura-0.0.1.dist-info/METADATA +0 -63
- oscura-0.0.1.dist-info/RECORD +0 -5
- {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/WHEEL +0 -0
oscura/core/logging.py
ADDED
|
@@ -0,0 +1,931 @@
|
|
|
1
|
+
"""Structured logging infrastructure for TraceKit.
|
|
2
|
+
|
|
3
|
+
This module provides structured logging with JSON/logfmt support,
|
|
4
|
+
hierarchical loggers, log rotation, and error context capture.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.core.logging import configure_logging, get_logger
|
|
9
|
+
>>> configure_logging(format='json', level='INFO')
|
|
10
|
+
>>> logger = get_logger('oscura.loaders')
|
|
11
|
+
>>> logger.info("Loading trace", file="data.bin", size_mb=1024)
|
|
12
|
+
|
|
13
|
+
References:
|
|
14
|
+
Python logging module best practices
|
|
15
|
+
LOG-001 through LOG-008 requirements
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import gzip
|
|
21
|
+
import json
|
|
22
|
+
import logging
|
|
23
|
+
import logging.handlers
|
|
24
|
+
import os
|
|
25
|
+
import shutil
|
|
26
|
+
import sys
|
|
27
|
+
import time
|
|
28
|
+
import traceback
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
from datetime import UTC, datetime
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import Any, Literal
|
|
33
|
+
|
|
34
|
+
# Global logging configuration
|
|
35
|
+
_logging_configured = False
|
|
36
|
+
_root_logger_name = "oscura"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class LogConfig:
|
|
41
|
+
"""Logging configuration.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
level: Default log level (DEBUG, INFO, WARNING, ERROR, CRITICAL).
|
|
45
|
+
format: Output format (json, logfmt, text).
|
|
46
|
+
timestamp_format: Timestamp format (iso8601, iso8601_local, unix, custom).
|
|
47
|
+
custom_timestamp_format: Custom timestamp format string.
|
|
48
|
+
console_output: Enable console output to stderr.
|
|
49
|
+
file_output: Enable file output.
|
|
50
|
+
file_path: Path to log file.
|
|
51
|
+
max_bytes: Maximum log file size before rotation.
|
|
52
|
+
backup_count: Number of rotated log files to keep.
|
|
53
|
+
compress: Compress rotated log files.
|
|
54
|
+
when: Time-based rotation interval type ('midnight', 'H', 'D', 'W0'-'W6').
|
|
55
|
+
interval: Interval for time-based rotation.
|
|
56
|
+
max_age: Maximum age for log files (e.g., '30d').
|
|
57
|
+
|
|
58
|
+
References:
|
|
59
|
+
LOG-001: Structured Logging Framework
|
|
60
|
+
LOG-003: Automatic Log Rotation and Retention Policies
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
level: str = "INFO"
|
|
64
|
+
format: Literal["json", "logfmt", "text"] = "text"
|
|
65
|
+
timestamp_format: Literal["iso8601", "iso8601_local", "unix", "custom"] = "iso8601"
|
|
66
|
+
custom_timestamp_format: str | None = None
|
|
67
|
+
console_output: bool = True
|
|
68
|
+
file_output: bool = False
|
|
69
|
+
file_path: str | None = None
|
|
70
|
+
max_bytes: int = 10_000_000 # 10 MB
|
|
71
|
+
backup_count: int = 5
|
|
72
|
+
compress: bool = False
|
|
73
|
+
when: str | None = None # Time-based rotation: 'midnight', 'H', 'D', 'W0'-'W6'
|
|
74
|
+
interval: int = 1 # Interval for time-based rotation
|
|
75
|
+
max_age: str | None = None # Maximum age for log files (e.g., '30d')
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# Default configuration
|
|
79
|
+
_config = LogConfig()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class CompressingRotatingFileHandler(logging.handlers.RotatingFileHandler):
|
|
83
|
+
"""RotatingFileHandler that compresses rotated files with gzip.
|
|
84
|
+
|
|
85
|
+
Extends standard RotatingFileHandler to optionally compress log files
|
|
86
|
+
when they are rotated, saving disk space for historical logs.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
filename: Path to log file.
|
|
90
|
+
mode: File open mode.
|
|
91
|
+
maxBytes: Maximum file size before rotation.
|
|
92
|
+
backupCount: Number of backup files to keep.
|
|
93
|
+
encoding: File encoding.
|
|
94
|
+
compress: Whether to gzip compress rotated files.
|
|
95
|
+
|
|
96
|
+
References:
|
|
97
|
+
LOG-003: Automatic Log Rotation and Retention Policies
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
def __init__(
|
|
101
|
+
self,
|
|
102
|
+
filename: str,
|
|
103
|
+
mode: str = "a",
|
|
104
|
+
maxBytes: int = 0,
|
|
105
|
+
backupCount: int = 0,
|
|
106
|
+
encoding: str | None = None,
|
|
107
|
+
compress: bool = False,
|
|
108
|
+
):
|
|
109
|
+
"""Initialize compressing rotating file handler.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
filename: Path to log file.
|
|
113
|
+
mode: File open mode.
|
|
114
|
+
maxBytes: Maximum file size before rotation.
|
|
115
|
+
backupCount: Number of backup files to keep.
|
|
116
|
+
encoding: File encoding.
|
|
117
|
+
compress: Whether to gzip compress rotated files.
|
|
118
|
+
"""
|
|
119
|
+
super().__init__(filename, mode, maxBytes, backupCount, encoding)
|
|
120
|
+
self.compress = compress
|
|
121
|
+
|
|
122
|
+
def doRollover(self) -> None:
|
|
123
|
+
"""Perform rollover and optionally compress the rotated file.
|
|
124
|
+
|
|
125
|
+
References:
|
|
126
|
+
LOG-003: Automatic Log Rotation and Retention Policies
|
|
127
|
+
"""
|
|
128
|
+
# Standard rollover
|
|
129
|
+
super().doRollover()
|
|
130
|
+
|
|
131
|
+
# Compress the rolled file if compression is enabled
|
|
132
|
+
if self.compress and self.backupCount > 0:
|
|
133
|
+
# The most recently rotated file is .1
|
|
134
|
+
rotated_file = f"{self.baseFilename}.1"
|
|
135
|
+
compressed_file = f"{rotated_file}.gz"
|
|
136
|
+
|
|
137
|
+
if Path(rotated_file).exists():
|
|
138
|
+
# Compress the file
|
|
139
|
+
with open(rotated_file, "rb") as f_in:
|
|
140
|
+
with gzip.open(compressed_file, "wb") as f_out:
|
|
141
|
+
shutil.copyfileobj(f_in, f_out)
|
|
142
|
+
|
|
143
|
+
# Remove the uncompressed file
|
|
144
|
+
Path(rotated_file).unlink()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class CompressingTimedRotatingFileHandler(logging.handlers.TimedRotatingFileHandler):
|
|
148
|
+
"""TimedRotatingFileHandler that compresses rotated files with gzip.
|
|
149
|
+
|
|
150
|
+
Extends standard TimedRotatingFileHandler to optionally compress log files
|
|
151
|
+
when they are rotated, saving disk space for historical logs.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
filename: Path to log file.
|
|
155
|
+
when: Type of interval ('midnight', 'H', 'D', 'W0'-'W6').
|
|
156
|
+
interval: Number of intervals between rotations.
|
|
157
|
+
backupCount: Number of backup files to keep.
|
|
158
|
+
encoding: File encoding.
|
|
159
|
+
compress: Whether to gzip compress rotated files.
|
|
160
|
+
max_age: Maximum age for log files (e.g., '30d').
|
|
161
|
+
|
|
162
|
+
References:
|
|
163
|
+
LOG-003: Automatic Log Rotation and Retention Policies
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
def __init__(
|
|
167
|
+
self,
|
|
168
|
+
filename: str,
|
|
169
|
+
when: str = "midnight",
|
|
170
|
+
interval: int = 1,
|
|
171
|
+
backupCount: int = 0,
|
|
172
|
+
encoding: str | None = None,
|
|
173
|
+
compress: bool = False,
|
|
174
|
+
max_age: str | None = None,
|
|
175
|
+
):
|
|
176
|
+
"""Initialize compressing timed rotating file handler.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
filename: Path to log file.
|
|
180
|
+
when: Type of interval ('midnight', 'H', 'D', 'W0'-'W6').
|
|
181
|
+
interval: Number of intervals between rotations.
|
|
182
|
+
backupCount: Number of backup files to keep.
|
|
183
|
+
encoding: File encoding.
|
|
184
|
+
compress: Whether to gzip compress rotated files.
|
|
185
|
+
max_age: Maximum age for log files (e.g., '30d').
|
|
186
|
+
"""
|
|
187
|
+
super().__init__(filename, when, interval, backupCount, encoding=encoding)
|
|
188
|
+
self.compress = compress
|
|
189
|
+
self.max_age = max_age
|
|
190
|
+
self._max_age_seconds = self._parse_max_age(max_age) if max_age else None
|
|
191
|
+
|
|
192
|
+
def _parse_max_age(self, max_age: str) -> int:
|
|
193
|
+
"""Parse max_age string to seconds.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
max_age: Age string like '30d', '7d', '24h'.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Number of seconds.
|
|
200
|
+
|
|
201
|
+
Raises:
|
|
202
|
+
ValueError: If max_age format is invalid.
|
|
203
|
+
"""
|
|
204
|
+
if max_age.endswith("d"):
|
|
205
|
+
return int(max_age[:-1]) * 86400 # days to seconds
|
|
206
|
+
elif max_age.endswith("h"):
|
|
207
|
+
return int(max_age[:-1]) * 3600 # hours to seconds
|
|
208
|
+
elif max_age.endswith("m"):
|
|
209
|
+
return int(max_age[:-1]) * 60 # minutes to seconds
|
|
210
|
+
else:
|
|
211
|
+
raise ValueError(f"Invalid max_age format: {max_age}. Use 'd', 'h', or 'm' suffix.")
|
|
212
|
+
|
|
213
|
+
def doRollover(self) -> None:
|
|
214
|
+
"""Perform rollover and optionally compress the rotated file.
|
|
215
|
+
|
|
216
|
+
Also cleans up files older than max_age if specified.
|
|
217
|
+
|
|
218
|
+
References:
|
|
219
|
+
LOG-003: Automatic Log Rotation and Retention Policies
|
|
220
|
+
"""
|
|
221
|
+
# Close stream before rollover
|
|
222
|
+
if self.stream is not None:
|
|
223
|
+
self.stream.close()
|
|
224
|
+
self.stream = None # type: ignore[assignment]
|
|
225
|
+
|
|
226
|
+
# Determine the file that just got rotated
|
|
227
|
+
current_time = int(self.rolloverAt - self.interval)
|
|
228
|
+
time_tuple = time.gmtime(current_time) if self.utc else time.localtime(current_time)
|
|
229
|
+
dfn = self.rotation_filename(
|
|
230
|
+
self.baseFilename + "." + self.suffix % time_tuple[:6] # type: ignore[arg-type]
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Handle the existing rotated file
|
|
234
|
+
if Path(dfn).exists():
|
|
235
|
+
Path(dfn).unlink()
|
|
236
|
+
|
|
237
|
+
# Rotate the current file
|
|
238
|
+
self.rotate(self.baseFilename, dfn)
|
|
239
|
+
|
|
240
|
+
# Compress if enabled
|
|
241
|
+
if self.compress and Path(dfn).exists():
|
|
242
|
+
compressed_file = f"{dfn}.gz"
|
|
243
|
+
with open(dfn, "rb") as f_in, gzip.open(compressed_file, "wb") as f_out:
|
|
244
|
+
shutil.copyfileobj(f_in, f_out)
|
|
245
|
+
Path(dfn).unlink()
|
|
246
|
+
|
|
247
|
+
# Clean up old files based on max_age
|
|
248
|
+
if self._max_age_seconds:
|
|
249
|
+
self._cleanup_old_files()
|
|
250
|
+
|
|
251
|
+
# Delete old files based on backupCount
|
|
252
|
+
if self.backupCount > 0:
|
|
253
|
+
self._delete_old_files()
|
|
254
|
+
|
|
255
|
+
# Set next rollover time
|
|
256
|
+
new_rollover_at = self.computeRollover(current_time)
|
|
257
|
+
while new_rollover_at <= self.rolloverAt:
|
|
258
|
+
new_rollover_at = new_rollover_at + self.interval
|
|
259
|
+
self.rolloverAt = new_rollover_at
|
|
260
|
+
|
|
261
|
+
# Open new log file
|
|
262
|
+
if not self.delay:
|
|
263
|
+
self.stream = self._open()
|
|
264
|
+
|
|
265
|
+
def _cleanup_old_files(self) -> None:
|
|
266
|
+
"""Remove log files older than max_age.
|
|
267
|
+
|
|
268
|
+
References:
|
|
269
|
+
LOG-003: Automatic Log Rotation and Retention Policies
|
|
270
|
+
"""
|
|
271
|
+
if not self._max_age_seconds:
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
now = datetime.now().timestamp()
|
|
275
|
+
base_path = Path(self.baseFilename)
|
|
276
|
+
log_dir = base_path.parent
|
|
277
|
+
base_name = base_path.name
|
|
278
|
+
|
|
279
|
+
for log_file in log_dir.glob(f"{base_name}.*"):
|
|
280
|
+
try:
|
|
281
|
+
file_age = now - log_file.stat().st_mtime
|
|
282
|
+
if file_age > self._max_age_seconds:
|
|
283
|
+
log_file.unlink()
|
|
284
|
+
except OSError:
|
|
285
|
+
pass # Ignore errors during cleanup
|
|
286
|
+
|
|
287
|
+
def _delete_old_files(self) -> None:
|
|
288
|
+
"""Delete files exceeding backup count.
|
|
289
|
+
|
|
290
|
+
References:
|
|
291
|
+
LOG-003: Automatic Log Rotation and Retention Policies
|
|
292
|
+
"""
|
|
293
|
+
base_path = Path(self.baseFilename)
|
|
294
|
+
log_dir = base_path.parent
|
|
295
|
+
base_name = base_path.name
|
|
296
|
+
|
|
297
|
+
# Get all rotated files
|
|
298
|
+
rotated_files = sorted(
|
|
299
|
+
log_dir.glob(f"{base_name}.*"),
|
|
300
|
+
key=lambda p: p.stat().st_mtime,
|
|
301
|
+
reverse=True,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Remove files beyond backup count
|
|
305
|
+
for old_file in rotated_files[self.backupCount :]:
|
|
306
|
+
try:
|
|
307
|
+
old_file.unlink()
|
|
308
|
+
except OSError:
|
|
309
|
+
pass
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class StructuredFormatter(logging.Formatter):
|
|
313
|
+
"""Formatter that produces structured log output (JSON or logfmt).
|
|
314
|
+
|
|
315
|
+
Supports multiple output formats with ISO 8601 timestamps and
|
|
316
|
+
automatic correlation ID injection.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
fmt: Output format (json, logfmt, text).
|
|
320
|
+
timestamp_format: Timestamp format (iso8601, iso8601_local, unix).
|
|
321
|
+
|
|
322
|
+
References:
|
|
323
|
+
LOG-001: Structured Logging Framework
|
|
324
|
+
LOG-005: ISO 8601 Timestamps
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
def __init__(
|
|
328
|
+
self,
|
|
329
|
+
fmt: Literal["json", "logfmt", "text"] = "text",
|
|
330
|
+
timestamp_format: str = "iso8601",
|
|
331
|
+
):
|
|
332
|
+
"""Initialize structured formatter.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
fmt: Output format.
|
|
336
|
+
timestamp_format: Timestamp format.
|
|
337
|
+
"""
|
|
338
|
+
super().__init__()
|
|
339
|
+
self.fmt = fmt
|
|
340
|
+
self.timestamp_format = timestamp_format
|
|
341
|
+
|
|
342
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
343
|
+
"""Format log record as structured output.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
record: Log record to format.
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
Formatted log string.
|
|
350
|
+
"""
|
|
351
|
+
# Get timestamp
|
|
352
|
+
timestamp = self._format_timestamp(record.created)
|
|
353
|
+
|
|
354
|
+
# Build structured data
|
|
355
|
+
data = {
|
|
356
|
+
"timestamp": timestamp,
|
|
357
|
+
"level": record.levelname,
|
|
358
|
+
"module": record.name,
|
|
359
|
+
"message": record.getMessage(),
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
# Add correlation ID if present
|
|
363
|
+
try:
|
|
364
|
+
from oscura.core.correlation import get_correlation_id
|
|
365
|
+
|
|
366
|
+
corr_id = get_correlation_id()
|
|
367
|
+
if corr_id:
|
|
368
|
+
data["correlation_id"] = corr_id
|
|
369
|
+
except ImportError:
|
|
370
|
+
pass # Correlation module not yet loaded
|
|
371
|
+
|
|
372
|
+
# Add extra fields
|
|
373
|
+
for key, value in record.__dict__.items():
|
|
374
|
+
if key not in (
|
|
375
|
+
"name",
|
|
376
|
+
"msg",
|
|
377
|
+
"args",
|
|
378
|
+
"levelname",
|
|
379
|
+
"levelno",
|
|
380
|
+
"pathname",
|
|
381
|
+
"filename",
|
|
382
|
+
"module",
|
|
383
|
+
"exc_info",
|
|
384
|
+
"exc_text",
|
|
385
|
+
"stack_info",
|
|
386
|
+
"lineno",
|
|
387
|
+
"funcName",
|
|
388
|
+
"created",
|
|
389
|
+
"msecs",
|
|
390
|
+
"relativeCreated",
|
|
391
|
+
"thread",
|
|
392
|
+
"threadName",
|
|
393
|
+
"processName",
|
|
394
|
+
"process",
|
|
395
|
+
"message",
|
|
396
|
+
"asctime",
|
|
397
|
+
):
|
|
398
|
+
data[key] = value
|
|
399
|
+
|
|
400
|
+
# Add exception info if present
|
|
401
|
+
if record.exc_info:
|
|
402
|
+
data["exception"] = self.formatException(record.exc_info)
|
|
403
|
+
|
|
404
|
+
if self.fmt == "json":
|
|
405
|
+
return json.dumps(data, default=str)
|
|
406
|
+
elif self.fmt == "logfmt":
|
|
407
|
+
return self._format_logfmt(data)
|
|
408
|
+
else:
|
|
409
|
+
# Plain text format
|
|
410
|
+
extra = " ".join(
|
|
411
|
+
f"{k}={v}"
|
|
412
|
+
for k, v in data.items()
|
|
413
|
+
if k not in ("timestamp", "level", "module", "message")
|
|
414
|
+
)
|
|
415
|
+
base = f"{timestamp} [{record.levelname}] {record.name}: {record.getMessage()}"
|
|
416
|
+
if extra:
|
|
417
|
+
base += f" | {extra}"
|
|
418
|
+
return base
|
|
419
|
+
|
|
420
|
+
def _format_timestamp(self, created: float) -> str:
|
|
421
|
+
"""Format timestamp according to configuration.
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
created: Timestamp as float (seconds since epoch).
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
Formatted timestamp string.
|
|
428
|
+
|
|
429
|
+
References:
|
|
430
|
+
LOG-005: ISO 8601 Timestamps
|
|
431
|
+
"""
|
|
432
|
+
dt = datetime.fromtimestamp(created, tz=UTC)
|
|
433
|
+
if self.timestamp_format == "iso8601":
|
|
434
|
+
# ISO 8601 with microseconds: 2025-12-20T15:30:45.123456Z
|
|
435
|
+
return dt.strftime("%Y-%m-%dT%H:%M:%S.%f") + "Z"
|
|
436
|
+
elif self.timestamp_format == "iso8601_local":
|
|
437
|
+
dt_local = datetime.fromtimestamp(created)
|
|
438
|
+
return dt_local.strftime("%Y-%m-%dT%H:%M:%S.%f")
|
|
439
|
+
elif self.timestamp_format == "unix":
|
|
440
|
+
return str(created)
|
|
441
|
+
else:
|
|
442
|
+
return dt.strftime(self.timestamp_format)
|
|
443
|
+
|
|
444
|
+
def _format_logfmt(self, data: dict) -> str: # type: ignore[type-arg]
|
|
445
|
+
"""Format data as logfmt (key=value pairs).
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
data: Dictionary to format.
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
Logfmt formatted string.
|
|
452
|
+
"""
|
|
453
|
+
parts = []
|
|
454
|
+
for key, value in data.items():
|
|
455
|
+
if isinstance(value, str) and (" " in value or '"' in value):
|
|
456
|
+
# Quote values with spaces
|
|
457
|
+
value_str = f'"{value.replace(chr(34), chr(92) + chr(34))}"'
|
|
458
|
+
else:
|
|
459
|
+
value_str = str(value)
|
|
460
|
+
parts.append(f"{key}={value_str}")
|
|
461
|
+
return " ".join(parts)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def configure_logging(
|
|
465
|
+
*,
|
|
466
|
+
level: str = "INFO",
|
|
467
|
+
format: Literal["json", "logfmt", "text"] = "text",
|
|
468
|
+
timestamp_format: Literal["iso8601", "iso8601_local", "unix"] = "iso8601",
|
|
469
|
+
handlers: dict[str, dict[str, Any]] | None = None,
|
|
470
|
+
) -> None:
|
|
471
|
+
"""Configure TraceKit logging.
|
|
472
|
+
|
|
473
|
+
Sets up structured logging with the specified format and handlers.
|
|
474
|
+
Supports both size-based and time-based log rotation with optional
|
|
475
|
+
gzip compression.
|
|
476
|
+
|
|
477
|
+
Args:
|
|
478
|
+
level: Default log level (DEBUG, INFO, WARNING, ERROR, CRITICAL).
|
|
479
|
+
format: Output format (json, logfmt, or text).
|
|
480
|
+
timestamp_format: Timestamp format (iso8601, iso8601_local, unix).
|
|
481
|
+
handlers: Dict of handler configurations. Supported handlers:
|
|
482
|
+
- 'console': Console output to stderr.
|
|
483
|
+
- level: Log level for this handler.
|
|
484
|
+
- 'file': File output with rotation.
|
|
485
|
+
- filename: Path to log file.
|
|
486
|
+
- level: Log level for this handler.
|
|
487
|
+
- max_bytes: Max file size before rotation (size-based).
|
|
488
|
+
- backup_count: Number of rotated files to keep.
|
|
489
|
+
- compress: Gzip compress rotated files.
|
|
490
|
+
- when: Time-based rotation ('midnight', 'H', 'D', 'W0'-'W6').
|
|
491
|
+
- interval: Interval for time-based rotation.
|
|
492
|
+
- max_age: Max age for log files (e.g., '30d').
|
|
493
|
+
|
|
494
|
+
Example:
|
|
495
|
+
>>> # Size-based rotation with compression
|
|
496
|
+
>>> configure_logging(handlers={
|
|
497
|
+
... 'file': {'filename': 'app.log', 'max_bytes': 10e6, 'compress': True}
|
|
498
|
+
... })
|
|
499
|
+
>>> # Time-based daily rotation
|
|
500
|
+
>>> configure_logging(handlers={
|
|
501
|
+
... 'file': {'filename': 'app.log', 'when': 'midnight', 'backup_count': 30}
|
|
502
|
+
... })
|
|
503
|
+
>>> # Combined: time-based with max_age cleanup
|
|
504
|
+
>>> configure_logging(handlers={
|
|
505
|
+
... 'file': {'filename': 'app.log', 'when': 'midnight',
|
|
506
|
+
... 'compress': True, 'max_age': '30d'}
|
|
507
|
+
... })
|
|
508
|
+
|
|
509
|
+
References:
|
|
510
|
+
LOG-001: Structured Logging Framework
|
|
511
|
+
LOG-002: Hierarchical Log Levels
|
|
512
|
+
LOG-003: Automatic Log Rotation and Retention Policies
|
|
513
|
+
"""
|
|
514
|
+
global _logging_configured, _config # noqa: PLW0602
|
|
515
|
+
|
|
516
|
+
# Update config
|
|
517
|
+
_config.level = level
|
|
518
|
+
_config.format = format
|
|
519
|
+
_config.timestamp_format = timestamp_format
|
|
520
|
+
|
|
521
|
+
# Get or create root logger
|
|
522
|
+
root_logger = logging.getLogger(_root_logger_name)
|
|
523
|
+
root_logger.setLevel(getattr(logging, level.upper()))
|
|
524
|
+
|
|
525
|
+
# Remove existing handlers and close them to prevent resource leaks
|
|
526
|
+
for handler in root_logger.handlers[:]:
|
|
527
|
+
handler.close()
|
|
528
|
+
root_logger.removeHandler(handler)
|
|
529
|
+
|
|
530
|
+
# Create formatter
|
|
531
|
+
formatter = StructuredFormatter(format, timestamp_format)
|
|
532
|
+
|
|
533
|
+
# Add handlers
|
|
534
|
+
if handlers:
|
|
535
|
+
for name, config in handlers.items():
|
|
536
|
+
if name == "console":
|
|
537
|
+
handler = logging.StreamHandler(sys.stderr)
|
|
538
|
+
handler.setLevel(getattr(logging, config.get("level", level).upper()))
|
|
539
|
+
handler.setFormatter(formatter)
|
|
540
|
+
root_logger.addHandler(handler)
|
|
541
|
+
elif name == "file":
|
|
542
|
+
filename = config.get("filename", "oscura.log")
|
|
543
|
+
handler_level = config.get("level", "DEBUG")
|
|
544
|
+
backup_count = int(config.get("backup_count", 5))
|
|
545
|
+
compress = config.get("compress", False)
|
|
546
|
+
|
|
547
|
+
# Check if time-based rotation is requested
|
|
548
|
+
when = config.get("when")
|
|
549
|
+
if when:
|
|
550
|
+
# Time-based rotation (LOG-003)
|
|
551
|
+
interval = int(config.get("interval", 1))
|
|
552
|
+
max_age = config.get("max_age")
|
|
553
|
+
handler = CompressingTimedRotatingFileHandler(
|
|
554
|
+
filename,
|
|
555
|
+
when=when,
|
|
556
|
+
interval=interval,
|
|
557
|
+
backupCount=backup_count,
|
|
558
|
+
compress=compress,
|
|
559
|
+
max_age=max_age,
|
|
560
|
+
)
|
|
561
|
+
else:
|
|
562
|
+
# Size-based rotation
|
|
563
|
+
max_bytes = int(config.get("max_bytes", 10_000_000))
|
|
564
|
+
handler = CompressingRotatingFileHandler(
|
|
565
|
+
filename,
|
|
566
|
+
maxBytes=max_bytes,
|
|
567
|
+
backupCount=backup_count,
|
|
568
|
+
compress=compress,
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
handler.setLevel(getattr(logging, handler_level.upper()))
|
|
572
|
+
handler.setFormatter(formatter)
|
|
573
|
+
root_logger.addHandler(handler)
|
|
574
|
+
else:
|
|
575
|
+
# Default: console only
|
|
576
|
+
handler = logging.StreamHandler(sys.stderr)
|
|
577
|
+
handler.setLevel(getattr(logging, level.upper()))
|
|
578
|
+
handler.setFormatter(formatter)
|
|
579
|
+
root_logger.addHandler(handler)
|
|
580
|
+
|
|
581
|
+
_logging_configured = True
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def get_logger(name: str) -> logging.Logger:
|
|
585
|
+
"""Get a logger with the specified name.
|
|
586
|
+
|
|
587
|
+
Returns a logger under the oscura namespace with proper
|
|
588
|
+
configuration.
|
|
589
|
+
|
|
590
|
+
Args:
|
|
591
|
+
name: Logger name (e.g., 'oscura.loaders.binary').
|
|
592
|
+
|
|
593
|
+
Returns:
|
|
594
|
+
Configured logging.Logger instance.
|
|
595
|
+
|
|
596
|
+
Example:
|
|
597
|
+
>>> logger = get_logger('oscura.analyzers.spectral')
|
|
598
|
+
>>> logger.info("Computing FFT", samples=1000000)
|
|
599
|
+
|
|
600
|
+
References:
|
|
601
|
+
LOG-001: Structured Logging Framework
|
|
602
|
+
"""
|
|
603
|
+
if not _logging_configured:
|
|
604
|
+
# Auto-configure with defaults
|
|
605
|
+
configure_logging()
|
|
606
|
+
|
|
607
|
+
# Ensure name is under oscura namespace
|
|
608
|
+
if not name.startswith(_root_logger_name):
|
|
609
|
+
name = f"{_root_logger_name}.{name}"
|
|
610
|
+
|
|
611
|
+
return logging.getLogger(name)
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def set_log_level(level: str, module: str | None = None) -> None:
|
|
615
|
+
"""Set log level globally or for a specific module.
|
|
616
|
+
|
|
617
|
+
Args:
|
|
618
|
+
level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL).
|
|
619
|
+
module: Module name to set level for, or None for global.
|
|
620
|
+
|
|
621
|
+
Example:
|
|
622
|
+
>>> set_log_level('DEBUG') # Global
|
|
623
|
+
>>> set_log_level('DEBUG', 'oscura.loaders') # Module-specific
|
|
624
|
+
|
|
625
|
+
References:
|
|
626
|
+
LOG-002: Hierarchical Log Levels
|
|
627
|
+
"""
|
|
628
|
+
logger = logging.getLogger(module) if module else logging.getLogger(_root_logger_name)
|
|
629
|
+
|
|
630
|
+
logger.setLevel(getattr(logging, level.upper()))
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
class ErrorContextCapture:
|
|
634
|
+
"""Captures rich error context including stack traces and local variables.
|
|
635
|
+
|
|
636
|
+
Provides detailed error information for debugging including:
|
|
637
|
+
- Full stack trace
|
|
638
|
+
- Local variables at each frame
|
|
639
|
+
- Exception chain
|
|
640
|
+
- System information
|
|
641
|
+
|
|
642
|
+
Example:
|
|
643
|
+
>>> try:
|
|
644
|
+
... risky_operation()
|
|
645
|
+
... except Exception as exc:
|
|
646
|
+
... context = ErrorContextCapture.from_exception(exc, include_locals=True)
|
|
647
|
+
... logger.error("Operation failed", extra=context.to_dict())
|
|
648
|
+
|
|
649
|
+
References:
|
|
650
|
+
LOG-008: Rich Error Context with Stack Traces
|
|
651
|
+
CORE-006: Helpful exception messages
|
|
652
|
+
"""
|
|
653
|
+
|
|
654
|
+
def __init__(
|
|
655
|
+
self,
|
|
656
|
+
exc_type: type[BaseException],
|
|
657
|
+
exc_value: BaseException,
|
|
658
|
+
exc_traceback: Any,
|
|
659
|
+
additional_context: dict[str, Any] | None = None,
|
|
660
|
+
):
|
|
661
|
+
"""Initialize error context capture.
|
|
662
|
+
|
|
663
|
+
Args:
|
|
664
|
+
exc_type: Exception type.
|
|
665
|
+
exc_value: Exception instance.
|
|
666
|
+
exc_traceback: Exception traceback.
|
|
667
|
+
additional_context: Additional context to include.
|
|
668
|
+
"""
|
|
669
|
+
self.exc_type = exc_type
|
|
670
|
+
self.exc_value = exc_value
|
|
671
|
+
self.exc_traceback = exc_traceback
|
|
672
|
+
self.additional_context = additional_context or {}
|
|
673
|
+
|
|
674
|
+
@classmethod
|
|
675
|
+
def from_exception(
|
|
676
|
+
cls,
|
|
677
|
+
exc: BaseException,
|
|
678
|
+
include_locals: bool = True, # noqa: ARG003
|
|
679
|
+
additional_context: dict[str, Any] | None = None,
|
|
680
|
+
) -> ErrorContextCapture:
|
|
681
|
+
"""Create error context from an exception.
|
|
682
|
+
|
|
683
|
+
Args:
|
|
684
|
+
exc: Exception to capture.
|
|
685
|
+
include_locals: Whether to include local variables.
|
|
686
|
+
additional_context: Additional context to include.
|
|
687
|
+
|
|
688
|
+
Returns:
|
|
689
|
+
ErrorContextCapture instance.
|
|
690
|
+
"""
|
|
691
|
+
exc_type = type(exc)
|
|
692
|
+
exc_value = exc
|
|
693
|
+
exc_traceback = exc.__traceback__
|
|
694
|
+
return cls(exc_type, exc_value, exc_traceback, additional_context)
|
|
695
|
+
|
|
696
|
+
def to_dict(self, include_locals: bool = True) -> dict[str, Any]:
|
|
697
|
+
"""Convert error context to dictionary.
|
|
698
|
+
|
|
699
|
+
Args:
|
|
700
|
+
include_locals: Whether to include local variables.
|
|
701
|
+
|
|
702
|
+
Returns:
|
|
703
|
+
Dictionary with error context.
|
|
704
|
+
|
|
705
|
+
References:
|
|
706
|
+
LOG-008: Rich Error Context
|
|
707
|
+
"""
|
|
708
|
+
result: dict[str, Any] = {
|
|
709
|
+
"exception_type": self.exc_type.__name__,
|
|
710
|
+
"exception_module": self.exc_type.__module__,
|
|
711
|
+
"exception_message": str(self.exc_value),
|
|
712
|
+
"traceback": traceback.format_exception(
|
|
713
|
+
self.exc_type, self.exc_value, self.exc_traceback
|
|
714
|
+
),
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
# Add exception chain
|
|
718
|
+
if hasattr(self.exc_value, "__cause__") and self.exc_value.__cause__:
|
|
719
|
+
result["caused_by"] = {
|
|
720
|
+
"type": type(self.exc_value.__cause__).__name__,
|
|
721
|
+
"message": str(self.exc_value.__cause__),
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
# Add local variables if requested
|
|
725
|
+
if include_locals and self.exc_traceback:
|
|
726
|
+
frames = []
|
|
727
|
+
tb = self.exc_traceback
|
|
728
|
+
while tb is not None:
|
|
729
|
+
frame = tb.tb_frame
|
|
730
|
+
frames.append(
|
|
731
|
+
{
|
|
732
|
+
"filename": frame.f_code.co_filename,
|
|
733
|
+
"function": frame.f_code.co_name,
|
|
734
|
+
"lineno": tb.tb_lineno,
|
|
735
|
+
"locals": self._filter_sensitive_data(
|
|
736
|
+
{k: repr(v) for k, v in frame.f_locals.items()}
|
|
737
|
+
),
|
|
738
|
+
}
|
|
739
|
+
)
|
|
740
|
+
tb = tb.tb_next
|
|
741
|
+
result["frames"] = frames
|
|
742
|
+
|
|
743
|
+
# Add additional context
|
|
744
|
+
if self.additional_context:
|
|
745
|
+
result["context"] = self.additional_context
|
|
746
|
+
|
|
747
|
+
return result
|
|
748
|
+
|
|
749
|
+
def _filter_sensitive_data(self, data: dict[str, str]) -> dict[str, str]:
|
|
750
|
+
"""Filter sensitive data from local variables.
|
|
751
|
+
|
|
752
|
+
Args:
|
|
753
|
+
data: Dictionary of local variables.
|
|
754
|
+
|
|
755
|
+
Returns:
|
|
756
|
+
Filtered dictionary with sensitive data redacted.
|
|
757
|
+
|
|
758
|
+
References:
|
|
759
|
+
LOG-008: Rich Error Context (sensitive data filtering)
|
|
760
|
+
"""
|
|
761
|
+
sensitive_keys = {
|
|
762
|
+
"password",
|
|
763
|
+
"passwd",
|
|
764
|
+
"pwd",
|
|
765
|
+
"secret",
|
|
766
|
+
"token",
|
|
767
|
+
"api_key",
|
|
768
|
+
"apikey",
|
|
769
|
+
"auth",
|
|
770
|
+
"authorization",
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
filtered = {}
|
|
774
|
+
for key, value in data.items():
|
|
775
|
+
key_lower = key.lower()
|
|
776
|
+
if any(sensitive in key_lower for sensitive in sensitive_keys):
|
|
777
|
+
filtered[key] = "***REDACTED***"
|
|
778
|
+
else:
|
|
779
|
+
filtered[key] = value
|
|
780
|
+
return filtered
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def log_exception(
|
|
784
|
+
exc: BaseException,
|
|
785
|
+
logger: logging.Logger | None = None,
|
|
786
|
+
context: dict[str, Any] | None = None,
|
|
787
|
+
include_locals: bool = False,
|
|
788
|
+
) -> None:
|
|
789
|
+
"""Log an exception with full context.
|
|
790
|
+
|
|
791
|
+
Captures rich error context including stack traces, exception chain,
|
|
792
|
+
and optionally local variables for debugging.
|
|
793
|
+
|
|
794
|
+
Args:
|
|
795
|
+
exc: The exception to log.
|
|
796
|
+
logger: Logger to use (default: root oscura logger).
|
|
797
|
+
context: Additional context to include.
|
|
798
|
+
include_locals: Whether to include local variables from stack frames.
|
|
799
|
+
|
|
800
|
+
Example:
|
|
801
|
+
>>> try:
|
|
802
|
+
... result = complex_computation(data)
|
|
803
|
+
... except Exception as e:
|
|
804
|
+
... log_exception(e, context={"data_size": len(data)})
|
|
805
|
+
|
|
806
|
+
References:
|
|
807
|
+
LOG-008: Rich Error Context with Stack Traces
|
|
808
|
+
CORE-006: Helpful exception messages
|
|
809
|
+
"""
|
|
810
|
+
if logger is None:
|
|
811
|
+
logger = get_logger("oscura")
|
|
812
|
+
|
|
813
|
+
# Capture error context
|
|
814
|
+
error_context = ErrorContextCapture.from_exception(
|
|
815
|
+
exc, include_locals=include_locals, additional_context=context
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
# Convert to dict and log
|
|
819
|
+
context_dict = error_context.to_dict(include_locals=include_locals)
|
|
820
|
+
|
|
821
|
+
# Log with exception info
|
|
822
|
+
logger.exception("Exception occurred", extra=context_dict)
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
def format_timestamp(
|
|
826
|
+
dt: datetime | None = None,
|
|
827
|
+
format: Literal["iso8601", "iso8601_local", "unix"] = "iso8601",
|
|
828
|
+
) -> str:
|
|
829
|
+
"""Format a timestamp according to LOG-005 requirements.
|
|
830
|
+
|
|
831
|
+
Args:
|
|
832
|
+
dt: Datetime to format, or None for current time.
|
|
833
|
+
format: Format to use (iso8601, iso8601_local, unix).
|
|
834
|
+
|
|
835
|
+
Returns:
|
|
836
|
+
Formatted timestamp string.
|
|
837
|
+
|
|
838
|
+
Raises:
|
|
839
|
+
ValueError: If format is unknown.
|
|
840
|
+
|
|
841
|
+
Example:
|
|
842
|
+
>>> ts = format_timestamp()
|
|
843
|
+
>>> print(ts) # 2025-12-20T15:30:45.123456Z
|
|
844
|
+
|
|
845
|
+
References:
|
|
846
|
+
LOG-005: ISO 8601 Timestamps
|
|
847
|
+
"""
|
|
848
|
+
if dt is None:
|
|
849
|
+
dt = datetime.now(UTC)
|
|
850
|
+
|
|
851
|
+
if format == "iso8601":
|
|
852
|
+
return dt.strftime("%Y-%m-%dT%H:%M:%S.%f") + "Z"
|
|
853
|
+
elif format == "iso8601_local":
|
|
854
|
+
dt_local = dt.astimezone()
|
|
855
|
+
return dt_local.strftime("%Y-%m-%dT%H:%M:%S.%f")
|
|
856
|
+
elif format == "unix":
|
|
857
|
+
return str(dt.timestamp())
|
|
858
|
+
else:
|
|
859
|
+
raise ValueError(f"Unknown timestamp format: {format}")
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
# Initialize logging on module import (with defaults)
|
|
863
|
+
def _init_logging() -> None:
|
|
864
|
+
"""Initialize logging with environment variable configuration.
|
|
865
|
+
|
|
866
|
+
Reads:
|
|
867
|
+
TRACEKIT_LOG_LEVEL: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
868
|
+
TRACEKIT_LOG_FORMAT: Log format (json, logfmt, text)
|
|
869
|
+
|
|
870
|
+
References:
|
|
871
|
+
LOG-001: Structured Logging Framework
|
|
872
|
+
LOG-002: Hierarchical Log Levels
|
|
873
|
+
"""
|
|
874
|
+
level = os.environ.get("TRACEKIT_LOG_LEVEL", "WARNING")
|
|
875
|
+
log_format = os.environ.get("TRACEKIT_LOG_FORMAT", "text")
|
|
876
|
+
|
|
877
|
+
if log_format in ("json", "logfmt", "text"):
|
|
878
|
+
configure_logging(level=level, format=log_format) # type: ignore[arg-type]
|
|
879
|
+
else:
|
|
880
|
+
configure_logging(level=level)
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
# Auto-initialize on import
|
|
884
|
+
_init_logging()
|
|
885
|
+
|
|
886
|
+
# Re-export correlation and performance functions for convenience
|
|
887
|
+
# These provide LOG-004 and LOG-006 functionality through this module
|
|
888
|
+
from oscura.core.correlation import ( # noqa: E402
|
|
889
|
+
CorrelationContext,
|
|
890
|
+
generate_correlation_id,
|
|
891
|
+
get_correlation_id,
|
|
892
|
+
set_correlation_id,
|
|
893
|
+
with_correlation_id,
|
|
894
|
+
)
|
|
895
|
+
from oscura.core.performance import ( # noqa: E402
|
|
896
|
+
PerformanceContext,
|
|
897
|
+
PerformanceRecord,
|
|
898
|
+
clear_performance_data,
|
|
899
|
+
get_performance_records,
|
|
900
|
+
get_performance_summary,
|
|
901
|
+
timed,
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
__all__ = [
|
|
905
|
+
"CompressingRotatingFileHandler",
|
|
906
|
+
"CompressingTimedRotatingFileHandler",
|
|
907
|
+
"CorrelationContext",
|
|
908
|
+
# Error handling (LOG-008)
|
|
909
|
+
"ErrorContextCapture",
|
|
910
|
+
"LogConfig",
|
|
911
|
+
"PerformanceContext",
|
|
912
|
+
"PerformanceRecord",
|
|
913
|
+
"StructuredFormatter",
|
|
914
|
+
"clear_performance_data",
|
|
915
|
+
# Logging configuration (LOG-001, LOG-002, LOG-003)
|
|
916
|
+
"configure_logging",
|
|
917
|
+
# Timestamps (LOG-005)
|
|
918
|
+
"format_timestamp",
|
|
919
|
+
"generate_correlation_id",
|
|
920
|
+
# Correlation ID (LOG-004)
|
|
921
|
+
"get_correlation_id",
|
|
922
|
+
"get_logger",
|
|
923
|
+
"get_performance_records",
|
|
924
|
+
"get_performance_summary",
|
|
925
|
+
"log_exception",
|
|
926
|
+
"set_correlation_id",
|
|
927
|
+
"set_log_level",
|
|
928
|
+
# Performance timing (LOG-006)
|
|
929
|
+
"timed",
|
|
930
|
+
"with_correlation_id",
|
|
931
|
+
]
|