SolarManager unter Versionsverwaltung

Erster Stand der Hintergrundprozesse, die auf der Synology unter
/volume1/homes/wagner/SolarManager laufen: der Manager selbst, die Sammler
je Geraet, die MQTT-Bruecke, der Wecker und - neu hinzugezogen - der
AutoAction-Runner, der als Hintergrundprozess hierher gehoert und nicht ins
Web-Verzeichnis.

Zugangsdaten stehen nicht mehr im Quelltext, sondern in config.ini, die
nicht mit eingecheckt wird. Vorlage ist config.ini.example, gelesen wird sie
von konfig.py. Betroffen waren solarManager.py (Datenbank und Wattpilot),
zeit.py, gatherWaterData.py, wecker.py und skoda_testdaten.py, das sich das
Passwort bisher aus dem Quelltext eines anderen Moduls herausgesucht hat.

Die Kia-Anbindung ist mit dem Fahrzeug entfallen: kiaTest.py,
gatherCarData.py und hyundai_kia_connect_api sind nicht mehr dabei, ebenso
gatherInverterData.py, auf das nur noch eine auskommentierte Zeile zeigte.

Die mitgelieferten Bibliotheken bleiben im Repository - die NAS hat kein
pip, sie muessen neben den Skripten liegen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 20:46:59 +02:00
co-authored by Claude Opus 5
commit 79843aa2ae
968 changed files with 261182 additions and 0 deletions
View File
@@ -0,0 +1,45 @@
Metadata-Version: 2.1
Name: opentelemetry-api
Version: 1.18.0
Summary: OpenTelemetry Python API
Project-URL: Homepage, https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-api
Author-email: OpenTelemetry Authors <cncf-opentelemetry-contributors@lists.cncf.io>
License-Expression: Apache-2.0
License-File: LICENSE
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Typing :: Typed
Requires-Python: >=3.7
Requires-Dist: deprecated>=1.2.6
Requires-Dist: importlib-metadata~=6.0.0
Requires-Dist: setuptools>=16.0
Provides-Extra: test
Description-Content-Type: text/x-rst
OpenTelemetry Python API
============================================================================
|pypi|
.. |pypi| image:: https://badge.fury.io/py/opentelemetry-api.svg
:target: https://pypi.org/project/opentelemetry-api/
Installation
------------
::
pip install opentelemetry-api
References
----------
* `OpenTelemetry Project <https://opentelemetry.io/>`_
@@ -0,0 +1,63 @@
opentelemetry/__pycache__/environment_variables.cpython-310.pyc,,
opentelemetry/__pycache__/version.cpython-310.pyc,,
opentelemetry/_logs/__init__.py,sha256=IAYAc_clcAbr_mfbnd2nlUqy29Oloe2ToY7kWJsD_ok,1980
opentelemetry/_logs/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/_logs/_internal/__init__.py,sha256=IsDxR65exZrtivs_dH0EXOFW_GIM2W9aPLttpprZvEA,7921
opentelemetry/_logs/_internal/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/_logs/severity/__init__.py,sha256=GIZVyH_D2_D7YOfX66T0EZnBEFT7HZeioD8FlHUu0Rs,3374
opentelemetry/_logs/severity/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/attributes/__init__.py,sha256=vtQCpL-oAYADbmKg_Vg6eAYTgkKmN-4e8Nt_JMu3r70,6648
opentelemetry/attributes/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/baggage/__init__.py,sha256=SDIJxXMfBQPkDBz4i-6nFLWeNvYLIHyqNYNXRsNsJDE,3875
opentelemetry/baggage/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/baggage/propagation/__init__.py,sha256=YVO2abxuRAAVIt2KsEEL7j32QQsWL5tFZKUp1MA5IAo,4677
opentelemetry/baggage/propagation/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/context/__init__.py,sha256=LJ3Ah4mqh8hMzZKZLrQv-GNfYJQym3RLn5zGHyIidSs,6148
opentelemetry/context/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/context/__pycache__/context.cpython-310.pyc,,
opentelemetry/context/__pycache__/contextvars_context.cpython-310.pyc,,
opentelemetry/context/context.py,sha256=NamBGlAlwMmplU4U8tgJXXIONfrGWdNunSJ99icHumA,1632
opentelemetry/context/contextvars_context.py,sha256=gtLd8IBhpRk1L3BJJmeITiQzat2lWZTBwZmjT9PXvy8,1785
opentelemetry/environment_variables.py,sha256=ryd23fCpZvZBE4PqMJW1P-DDpI70vj-hfN6IaO6DFDE,1433
opentelemetry/metrics/__init__.py,sha256=kCm2c9R5P04q2DbN_NLZzHlANr_baellq7Zo5ckrVWM,3366
opentelemetry/metrics/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/metrics/_internal/__init__.py,sha256=rI14PtLBI96fuW3vQIse9ef5Nzz2Dqc75-5fp7u0pUc,27216
opentelemetry/metrics/_internal/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/metrics/_internal/__pycache__/instrument.cpython-310.pyc,,
opentelemetry/metrics/_internal/__pycache__/observation.cpython-310.pyc,,
opentelemetry/metrics/_internal/instrument.py,sha256=NeV6Pw7Xr2F_f1zOQ2QOIsi5yjnbby5oE99M4qlD4g4,11438
opentelemetry/metrics/_internal/observation.py,sha256=WrzGscBXf_dboUhK3veiOUrJ9N7UUCvwqzJ0OIpXnuU,1600
opentelemetry/propagate/__init__.py,sha256=4tTE13xyrhjA0nzxxLJ7NXtIlBBdrVUM6ybmZnYfpC8,5546
opentelemetry/propagate/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/propagators/__pycache__/composite.cpython-310.pyc,,
opentelemetry/propagators/__pycache__/textmap.cpython-310.pyc,,
opentelemetry/propagators/composite.py,sha256=EgdgEbaNEN7g-XNGXR9YEO8akBv7eOWzA4pKyhDXVxc,3255
opentelemetry/propagators/textmap.py,sha256=IorrrDqoRv1ESB7Zn43lEeg7X_jetb6vNK87o-1oYSY,6611
opentelemetry/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
opentelemetry/trace/__init__.py,sha256=NpRR0QJ3BMH_8DL8tFO9f1eAOZANqRmiXftzrvFCfjo,21792
opentelemetry/trace/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/trace/__pycache__/span.cpython-310.pyc,,
opentelemetry/trace/__pycache__/status.cpython-310.pyc,,
opentelemetry/trace/propagation/__init__.py,sha256=YZMj0p-IcgBkyBfcZN0xO-3iUxi65Z8_zaIZGXRu5Q4,1684
opentelemetry/trace/propagation/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/trace/propagation/__pycache__/tracecontext.cpython-310.pyc,,
opentelemetry/trace/propagation/tracecontext.py,sha256=enrv8I99529sQcvokscqfZyY_Z6GblgV3r2W-rjxLTA,4178
opentelemetry/trace/span.py,sha256=Ks9wdxTut8VuNHd8tWKCjvXXU_veSF64CGmOzsnS884,18681
opentelemetry/trace/status.py,sha256=2K7fRLV7gDFAgpFA4AvMTjJfEUfyZjFa2PQ3VjjHBHE,2539
opentelemetry/util/__pycache__/_importlib_metadata.cpython-310.pyc,,
opentelemetry/util/__pycache__/_once.cpython-310.pyc,,
opentelemetry/util/__pycache__/_providers.cpython-310.pyc,,
opentelemetry/util/__pycache__/re.cpython-310.pyc,,
opentelemetry/util/__pycache__/types.cpython-310.pyc,,
opentelemetry/util/_importlib_metadata.py,sha256=Q-z72Ffut5iY0jdxDjgQyBrb6NYpUFKMEma4DEVCuxM,1135
opentelemetry/util/_once.py,sha256=qTsPYBYopTsAtVthY88gd8EQR6jNe-yWzZB353_REDY,1440
opentelemetry/util/_providers.py,sha256=RbrfX_WKVRHLwZvQwwCcU1XGYNMTTvwcJamfiSQkcMc,1695
opentelemetry/util/re.py,sha256=JhahUT7wybSbuWpmpQajxwW0ANHxda4eAQ1-mML3sZc,3057
opentelemetry/util/types.py,sha256=a9i0orW124UkS48cDIa0PDZOsjbx1weHHNJp3gGjlQc,1167
opentelemetry/version.py,sha256=VKe-G-_4rLUgSzj7TMERkPdV9nHLQk2pZAPptiOWIns,608
opentelemetry_api-1.18.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
opentelemetry_api-1.18.0.dist-info/METADATA,sha256=bId4BDNLdLPM0dceKP-YxRAUVscD-XOpguBUHRjcdQ4,1438
opentelemetry_api-1.18.0.dist-info/RECORD,,
opentelemetry_api-1.18.0.dist-info/WHEEL,sha256=y1bSCq4r5i4nMmpXeUJMqs3ipKvkZObrIXSvJHm1qCI,87
opentelemetry_api-1.18.0.dist-info/entry_points.txt,sha256=dxPq0YRbQDSwl8QkR-I9A38rbbfKQG5h2uNFjpvU6V4,573
opentelemetry_api-1.18.0.dist-info/licenses/LICENSE,sha256=h8jwqxShIeVkc8vOo9ynxGYW16f4fVPxLhZKZs0H5U8,11350
@@ -0,0 +1,4 @@
Wheel-Version: 1.0
Generator: hatchling 1.17.0
Root-Is-Purelib: true
Tag: py3-none-any
@@ -0,0 +1,15 @@
[opentelemetry_context]
contextvars_context = mysql.opentelemetry.context.contextvars_context:ContextVarsRuntimeContext
[opentelemetry_environment_variables]
api = mysql.opentelemetry.environment_variables
[opentelemetry_meter_provider]
default_meter_provider = mysql.opentelemetry.metrics:NoOpMeterProvider
[opentelemetry_propagator]
baggage = mysql.opentelemetry.baggage.propagation:W3CBaggagePropagator
tracecontext = mysql.opentelemetry.trace.propagation.tracecontext:TraceContextTextMapPropagator
[opentelemetry_tracer_provider]
default_tracer_provider = mysql.opentelemetry.trace:NoOpTracerProvider
@@ -0,0 +1,46 @@
Metadata-Version: 2.1
Name: opentelemetry-sdk
Version: 1.18.0
Summary: OpenTelemetry Python SDK
Project-URL: Homepage, https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-sdk
Author-email: OpenTelemetry Authors <cncf-opentelemetry-contributors@lists.cncf.io>
License-Expression: Apache-2.0
License-File: LICENSE
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Typing :: Typed
Requires-Python: >=3.7
Requires-Dist: opentelemetry-api==1.18.0
Requires-Dist: opentelemetry-semantic-conventions==0.39b0
Requires-Dist: setuptools>=16.0
Requires-Dist: typing-extensions>=3.7.4
Provides-Extra: test
Description-Content-Type: text/x-rst
OpenTelemetry Python SDK
============================================================================
|pypi|
.. |pypi| image:: https://badge.fury.io/py/opentelemetry-sdk.svg
:target: https://pypi.org/project/opentelemetry-sdk/
Installation
------------
::
pip install opentelemetry-sdk
References
----------
* `OpenTelemetry Project <https://opentelemetry.io/>`_
@@ -0,0 +1,86 @@
opentelemetry/sdk/__init__.pyi,sha256=kQMbMw8wLQtWJ1bVBm7XoI06B_4Fv0un5hv3FKwrgRQ,669
opentelemetry/sdk/__pycache__/environment_variables.cpython-310.pyc,,
opentelemetry/sdk/__pycache__/version.cpython-310.pyc,,
opentelemetry/sdk/_configuration/__init__.py,sha256=j6SnNJGoOuPTMJzaHoU27Qna8CnZtqY5qijhHL7_eT8,13196
opentelemetry/sdk/_configuration/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/sdk/_logs/__init__.py,sha256=cL14XplLJIhQzn3i9cQrTj4NQPeaLqmsIm75WXyMyLg,871
opentelemetry/sdk/_logs/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/sdk/_logs/_internal/__init__.py,sha256=_HhLIOa4QAyTEXVW3EfdcnBISZU6DVeMxuAtCp6OnOE,16064
opentelemetry/sdk/_logs/_internal/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/sdk/_logs/_internal/export/__init__.py,sha256=5xe_EMWRxYWtspLioaLwAJn4ZS_2BciHdJ6OJdodpkI,15303
opentelemetry/sdk/_logs/_internal/export/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/sdk/_logs/_internal/export/__pycache__/in_memory_log_exporter.cpython-310.pyc,,
opentelemetry/sdk/_logs/_internal/export/in_memory_log_exporter.py,sha256=bkVQmGnkkxX3wFDNM_6Aumjjpw7Jjnvfzel_59byIAU,1667
opentelemetry/sdk/_logs/export/__init__.py,sha256=nUHdXNgwqfDe0KoGkNBX7Xl_mo477iyK3N0D5BH9g2g,1120
opentelemetry/sdk/_logs/export/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/sdk/environment_variables.py,sha256=fuSKC-rUI9opNtv5yGjIGhyN3zm6L6T154IfbVy5zWs,23728
opentelemetry/sdk/error_handler/__init__.py,sha256=i6DkJw6IHN3yc_boO8FOle46zfZC5L0fcx7k1WFhOyQ,4639
opentelemetry/sdk/error_handler/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/sdk/metrics/__init__.py,sha256=TP2HEHdlj_rImfkWrTyosOrrqat1CEC38FU9Rafmv7I,1125
opentelemetry/sdk/metrics/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/sdk/metrics/_internal/__init__.py,sha256=iOU52ByY51gwHH4iDJQD4Si1bAjp4DKVPLH0vsKwMws,17500
opentelemetry/sdk/metrics/_internal/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/sdk/metrics/_internal/__pycache__/_view_instrument_match.cpython-310.pyc,,
opentelemetry/sdk/metrics/_internal/__pycache__/aggregation.cpython-310.pyc,,
opentelemetry/sdk/metrics/_internal/__pycache__/exceptions.cpython-310.pyc,,
opentelemetry/sdk/metrics/_internal/__pycache__/instrument.cpython-310.pyc,,
opentelemetry/sdk/metrics/_internal/__pycache__/measurement.cpython-310.pyc,,
opentelemetry/sdk/metrics/_internal/__pycache__/measurement_consumer.cpython-310.pyc,,
opentelemetry/sdk/metrics/_internal/__pycache__/metric_reader_storage.cpython-310.pyc,,
opentelemetry/sdk/metrics/_internal/__pycache__/point.cpython-310.pyc,,
opentelemetry/sdk/metrics/_internal/__pycache__/sdk_configuration.cpython-310.pyc,,
opentelemetry/sdk/metrics/_internal/__pycache__/view.cpython-310.pyc,,
opentelemetry/sdk/metrics/_internal/_view_instrument_match.py,sha256=wde6gdQTj-v_IsnkdsVkkpDUD5ZhNCYBlpBHykaR1g0,5298
opentelemetry/sdk/metrics/_internal/aggregation.py,sha256=6vUe5IkuWVaFiekFZDVIKxLhLZ2lzpY1qrhkN-tk9gQ,34835
opentelemetry/sdk/metrics/_internal/exceptions.py,sha256=_0bPg3suYoIXKJ7eCqG3S_gUKVcUAHp11vwThwp_yAg,675
opentelemetry/sdk/metrics/_internal/exponential_histogram/__pycache__/buckets.cpython-310.pyc,,
opentelemetry/sdk/metrics/_internal/exponential_histogram/buckets.py,sha256=4xtwFfhVb5vxyjmgbDFPTEozbF9T2BJRsPpyB2vpBWU,5303
opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py,sha256=IfoEYNaK1fWeFDIJ6jAAUzy57rNEaBrLxXve7955psA,3754
opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__pycache__/errors.cpython-310.pyc,,
opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__pycache__/exponent_mapping.cpython-310.pyc,,
opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__pycache__/ieee_754.cpython-310.pyc,,
opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__pycache__/logarithm_mapping.cpython-310.pyc,,
opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/errors.py,sha256=6Q6jfsVluEKp5R_9ECLW8mq3ZooyX0w9WVz5e-YAhuY,886
opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py,sha256=k70o6Fd6zedo4VcI1TOTKh2RurdaAUMRU837sd5kO54,6130
opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.md,sha256=viHOrkG8Dx6LYKm1QAPMTm5GNJzeL9rtUHK7uUFs9bQ,4980
opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py,sha256=s8bGxpmyn6MP98lIniAZ71hh1MFMq-ADyo16g8Dzeks,5494
opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/logarithm_mapping.py,sha256=qXN0ZalesyUgvyJx4bNZO_sd9mO_5oiqP4nWONQHnAU,5833
opentelemetry/sdk/metrics/_internal/export/__init__.py,sha256=hPY5fkHg21AokOsMeBug6O99PecWwhOV9ips5opfPis,20607
opentelemetry/sdk/metrics/_internal/export/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/sdk/metrics/_internal/instrument.py,sha256=BG84n68dpvT4m2C3DlkmjQJSnmwviJjQR0N8KoGvDUY,7882
opentelemetry/sdk/metrics/_internal/measurement.py,sha256=oFyLgrizpDBI4R8VCwPeBtlhZOK-C9Lxbr-PzgOF3ew,959
opentelemetry/sdk/metrics/_internal/measurement_consumer.py,sha256=Gls0GV0E_eIQu3W6W4dE1dvzWlDLlKFI_a0K4W3GRZE,4342
opentelemetry/sdk/metrics/_internal/metric_reader_storage.py,sha256=vwgWf3eW3fqX_ykDHofRRKQXNjeuA28zDMvrXpDajJE,11933
opentelemetry/sdk/metrics/_internal/point.py,sha256=D6EOzLzzuqN1tkWEzkz-xBwyLbY1JEozshiR9JYxIAE,7188
opentelemetry/sdk/metrics/_internal/sdk_configuration.py,sha256=JG77yWdEH_MHzUIbvS_W2PiXKlcwOSd5wTiWAM0ihJo,1020
opentelemetry/sdk/metrics/_internal/view.py,sha256=4cokABStzfSpHe8c7Y-nCn7OtYGDl0wBpwW-6ASv3rY,5968
opentelemetry/sdk/metrics/export/__init__.py,sha256=5GE7tf3Ig7r2xSS_1eCuLeFmUbaf6UncNecjE_RbrsA,1627
opentelemetry/sdk/metrics/export/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/sdk/metrics/view/__init__.py,sha256=kPqd6YQdIKp1AsO8li4TiYiAYvbTdKCZVl_fOHRAOkk,1130
opentelemetry/sdk/metrics/view/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
opentelemetry/sdk/resources/__init__.py,sha256=7meVj9nDZTuSSziXxcQ7-fhCpihY0AtnEZrzlwuzew0,14332
opentelemetry/sdk/resources/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/sdk/trace/__init__.py,sha256=9N6gXUPHETjOwQbslj72TReUqPFZrLLTingvrV0Jjoo,43082
opentelemetry/sdk/trace/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/sdk/trace/__pycache__/id_generator.cpython-310.pyc,,
opentelemetry/sdk/trace/__pycache__/sampling.cpython-310.pyc,,
opentelemetry/sdk/trace/export/__init__.py,sha256=KIQF6c7ufH7_g5rMA_wIROKAcDYEvoLKBi-R16Ryx9o,17742
opentelemetry/sdk/trace/export/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/sdk/trace/export/__pycache__/in_memory_span_exporter.cpython-310.pyc,,
opentelemetry/sdk/trace/export/in_memory_span_exporter.py,sha256=BHpu_R0mdwd69wh2QxXJ-qZaxdarETMXlVj2HWEk4x8,2026
opentelemetry/sdk/trace/id_generator.py,sha256=aW-YwsqTSXXNn4iQQFwhfp9M0D3AobxCqHlRW2KrjxI,1682
opentelemetry/sdk/trace/sampling.py,sha256=LjXgNvIPmKi6uGV7NdqST4owbRy_dWWKsj16G-2CQjM,16703
opentelemetry/sdk/util/__init__.py,sha256=MGABQOB9Mtm82Uv8f0RIwX739nQJtQEGTgsfBV5TpM8,4386
opentelemetry/sdk/util/__init__.pyi,sha256=RFOnfLwZeldVdlnlEzUJwjL8wqAUwHdJ4anf5P_oBoE,2227
opentelemetry/sdk/util/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/sdk/util/__pycache__/instrumentation.cpython-310.pyc,,
opentelemetry/sdk/util/instrumentation.py,sha256=M04crSpicNbaacSTxbT6szCHPIt0aw5qBHeMCdl0pPw,4144
opentelemetry/sdk/version.py,sha256=VKe-G-_4rLUgSzj7TMERkPdV9nHLQk2pZAPptiOWIns,608
opentelemetry_sdk-1.18.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
opentelemetry_sdk-1.18.0.dist-info/METADATA,sha256=f_3Ab1wb6D19I9HggzjyvFyQ9T1QnD_14YgSgGOPbw8,1503
opentelemetry_sdk-1.18.0.dist-info/RECORD,,
opentelemetry_sdk-1.18.0.dist-info/WHEEL,sha256=y1bSCq4r5i4nMmpXeUJMqs3ipKvkZObrIXSvJHm1qCI,87
opentelemetry_sdk-1.18.0.dist-info/entry_points.txt,sha256=PjoiLYsPeicEzv6YPkzoPXBarSUGbG_BSHW43qZCEFY,1348
opentelemetry_sdk-1.18.0.dist-info/licenses/LICENSE,sha256=h8jwqxShIeVkc8vOo9ynxGYW16f4fVPxLhZKZs0H5U8,11350
@@ -0,0 +1,4 @@
Wheel-Version: 1.0
Generator: hatchling 1.17.0
Root-Is-Purelib: true
Tag: py3-none-any
@@ -0,0 +1,35 @@
[opentelemetry_environment_variables]
sdk = mysql.opentelemetry.sdk.environment_variables
[opentelemetry_id_generator]
random = mysql.opentelemetry.sdk.trace.id_generator:RandomIdGenerator
[opentelemetry_logger_provider]
sdk_logger_provider = mysql.opentelemetry.sdk._logs:LoggerProvider
[opentelemetry_logs_exporter]
console = mysql.opentelemetry.sdk._logs.export:ConsoleLogExporter
[opentelemetry_meter_provider]
sdk_meter_provider = mysql.opentelemetry.sdk.metrics:MeterProvider
[opentelemetry_metrics_exporter]
console = mysql.opentelemetry.sdk.metrics.export:ConsoleMetricExporter
[opentelemetry_resource_detector]
otel = mysql.opentelemetry.sdk.resources:OTELResourceDetector
process = mysql.opentelemetry.sdk.resources:ProcessResourceDetector
[opentelemetry_tracer_provider]
sdk_tracer_provider = mysql.opentelemetry.sdk.trace:TracerProvider
[opentelemetry_traces_exporter]
console = mysql.opentelemetry.sdk.trace.export:ConsoleSpanExporter
[opentelemetry_traces_sampler]
always_off = mysql.opentelemetry.sdk.trace.sampling:_AlwaysOff
always_on = mysql.opentelemetry.sdk.trace.sampling:_AlwaysOn
parentbased_always_off = mysql.opentelemetry.sdk.trace.sampling:_ParentBasedAlwaysOff
parentbased_always_on = mysql.opentelemetry.sdk.trace.sampling:_ParentBasedAlwaysOn
parentbased_traceidratio = mysql.opentelemetry.sdk.trace.sampling:ParentBasedTraceIdRatio
traceidratio = mysql.opentelemetry.sdk.trace.sampling:TraceIdRatioBased
@@ -0,0 +1,59 @@
Metadata-Version: 2.1
Name: opentelemetry-semantic-conventions
Version: 0.39b0
Summary: OpenTelemetry Semantic Conventions
Project-URL: Homepage, https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-semantic-conventions
Author-email: OpenTelemetry Authors <cncf-opentelemetry-contributors@lists.cncf.io>
License-Expression: Apache-2.0
License-File: LICENSE
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Requires-Python: >=3.7
Provides-Extra: test
Description-Content-Type: text/x-rst
OpenTelemetry Semantic Conventions
==================================
|pypi|
.. |pypi| image:: https://badge.fury.io/py/opentelemetry-semantic-conventions.svg
:target: https://pypi.org/project/opentelemetry-semantic-conventions/
This library contains generated code for the semantic conventions defined by the OpenTelemetry specification.
Installation
------------
::
pip install opentelemetry-semantic-conventions
Code Generation
---------------
These files were generated automatically from code in semconv_.
To regenerate the code, run ``../scripts/semconv/generate.sh``.
To build against a new release or specific commit of opentelemetry-specification_,
update the ``SPEC_VERSION`` variable in
``../scripts/semconv/generate.sh``. Then run the script and commit the changes.
.. _opentelemetry-specification: https://github.com/open-telemetry/opentelemetry-specification
.. _semconv: https://github.com/open-telemetry/opentelemetry-python/tree/main/scripts/semconv
References
----------
* `OpenTelemetry Project <https://opentelemetry.io/>`_
* `OpenTelemetry Semantic Conventions YAML Definitions <https://github.com/open-telemetry/opentelemetry-specification/tree/main/semantic_conventions>`_
* `generate.sh script <https://github.com/open-telemetry/opentelemetry-python/blob/main/scripts/semconv/generate.sh>`_
@@ -0,0 +1,15 @@
opentelemetry/semconv/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
opentelemetry/semconv/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/semconv/__pycache__/version.cpython-310.pyc,,
opentelemetry/semconv/metrics/__init__.py,sha256=Cuzgg6Ub2T9GgSd_nHXnYhHqP5aAS0vh51ddsk-sOZA,1117
opentelemetry/semconv/metrics/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/semconv/resource/__init__.py,sha256=00ydHrPWGQIqp7lNqFTpRk2MNzpmef71MzjN1TmdCFg,21915
opentelemetry/semconv/resource/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/semconv/trace/__init__.py,sha256=r6agWFOT4XpeNLGVqgOnA5Jnnerbg07YyuMKycvtO_A,38732
opentelemetry/semconv/trace/__pycache__/__init__.cpython-310.pyc,,
opentelemetry/semconv/version.py,sha256=QifjHc5eaNaAASTORH0to-wDBlOL5qn89Qf3mIN6CY8,608
opentelemetry_semantic_conventions-0.39b0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
opentelemetry_semantic_conventions-0.39b0.dist-info/METADATA,sha256=g2cFHnm540znYhl13HKSyQ0czcdWMWzE6d-AeQQ6GWY,2306
opentelemetry_semantic_conventions-0.39b0.dist-info/RECORD,,
opentelemetry_semantic_conventions-0.39b0.dist-info/WHEEL,sha256=y1bSCq4r5i4nMmpXeUJMqs3ipKvkZObrIXSvJHm1qCI,87
opentelemetry_semantic_conventions-0.39b0.dist-info/licenses/LICENSE,sha256=h8jwqxShIeVkc8vOo9ynxGYW16f4fVPxLhZKZs0H5U8,11350
@@ -0,0 +1,4 @@
Wheel-Version: 1.0
Generator: hatchling 1.17.0
Root-Is-Purelib: true
Tag: py3-none-any
+60
View File
@@ -0,0 +1,60 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
The OpenTelemetry logging API describes the classes used to generate logs and events.
The :class:`.LoggerProvider` provides users access to the :class:`.Logger` which in
turn is used to create :class:`.Event` and :class:`.Log` objects.
This module provides abstract (i.e. unimplemented) classes required for
logging, and a concrete no-op implementation :class:`.NoOpLogger` that allows applications
to use the API package alone without a supporting implementation.
To get a logger, you need to provide the package name from which you are
calling the logging APIs to OpenTelemetry by calling `LoggerProvider.get_logger`
with the calling module name and the version of your package.
The following code shows how to obtain a logger using the global :class:`.LoggerProvider`::
from opentelemetry._logs import get_logger
logger = get_logger("example-logger")
.. versionadded:: 1.15.0
"""
from mysql.opentelemetry._logs._internal import (
Logger,
LoggerProvider,
LogRecord,
NoOpLogger,
NoOpLoggerProvider,
get_logger,
get_logger_provider,
set_logger_provider,
)
from mysql.opentelemetry._logs.severity import SeverityNumber, std_to_otel
__all__ = [
"Logger",
"LoggerProvider",
"LogRecord",
"NoOpLogger",
"NoOpLoggerProvider",
"get_logger",
"get_logger_provider",
"set_logger_provider",
"SeverityNumber",
"std_to_otel",
]
@@ -0,0 +1,227 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
The OpenTelemetry logging API describes the classes used to generate logs and events.
The :class:`.LoggerProvider` provides users access to the :class:`.Logger` which in
turn is used to create :class:`.Event` and :class:`.Log` objects.
This module provides abstract (i.e. unimplemented) classes required for
logging, and a concrete no-op implementation :class:`.NoOpLogger` that allows applications
to use the API package alone without a supporting implementation.
To get a logger, you need to provide the package name from which you are
calling the logging APIs to OpenTelemetry by calling `LoggerProvider.get_logger`
with the calling module name and the version of your package.
The following code shows how to obtain a logger using the global :class:`.LoggerProvider`::
from mysql.opentelemetry._logs import get_logger
logger = get_logger("example-logger")
.. versionadded:: 1.15.0
"""
from abc import ABC, abstractmethod
from logging import getLogger
from os import environ
from typing import Any, Optional, cast
from mysql.opentelemetry._logs.severity import SeverityNumber
from mysql.opentelemetry.environment_variables import _OTEL_PYTHON_LOGGER_PROVIDER
from mysql.opentelemetry.trace.span import TraceFlags
from mysql.opentelemetry.util._once import Once
from mysql.opentelemetry.util._providers import _load_provider
from mysql.opentelemetry.util.types import Attributes
_logger = getLogger(__name__)
class LogRecord(ABC):
"""A LogRecord instance represents an event being logged.
LogRecord instances are created and emitted via `Logger`
every time something is logged. They contain all the information
pertinent to the event being logged.
"""
def __init__(
self,
timestamp: Optional[int] = None,
observed_timestamp: Optional[int] = None,
trace_id: Optional[int] = None,
span_id: Optional[int] = None,
trace_flags: Optional["TraceFlags"] = None,
severity_text: Optional[str] = None,
severity_number: Optional[SeverityNumber] = None,
body: Optional[Any] = None,
attributes: Optional["Attributes"] = None,
):
self.timestamp = timestamp
self.observed_timestamp = observed_timestamp
self.trace_id = trace_id
self.span_id = span_id
self.trace_flags = trace_flags
self.severity_text = severity_text
self.severity_number = severity_number
self.body = body # type: ignore
self.attributes = attributes
class Logger(ABC):
"""Handles emitting events and logs via `LogRecord`."""
def __init__(
self,
name: str,
version: Optional[str] = None,
schema_url: Optional[str] = None,
) -> None:
super().__init__()
self._name = name
self._version = version
self._schema_url = schema_url
@abstractmethod
def emit(self, record: "LogRecord") -> None:
"""Emits a :class:`LogRecord` representing a log to the processing pipeline."""
class NoOpLogger(Logger):
"""The default Logger used when no Logger implementation is available.
All operations are no-op.
"""
def emit(self, record: "LogRecord") -> None:
pass
class LoggerProvider(ABC):
"""
LoggerProvider is the entry point of the API. It provides access to Logger instances.
"""
@abstractmethod
def get_logger(
self,
name: str,
version: Optional[str] = None,
schema_url: Optional[str] = None,
) -> Logger:
"""Returns a `Logger` for use by the given instrumentation library.
For any two calls it is undefined whether the same or different
`Logger` instances are returned, even for different library names.
This function may return different `Logger` types (e.g. a no-op logger
vs. a functional logger).
Args:
name: The name of the instrumenting module.
``__name__`` may not be used as this can result in
different logger names if the loggers are in different files.
It is better to use a fixed string that can be imported where
needed and used consistently as the name of the logger.
This should *not* be the name of the module that is
instrumented but the name of the module doing the instrumentation.
E.g., instead of ``"requests"``, use
``"mysql.opentelemetry.instrumentation.requests"``.
version: Optional. The version string of the
instrumenting library. Usually this should be the same as
``pkg_resources.get_distribution(instrumenting_library_name).version``.
schema_url: Optional. Specifies the Schema URL of the emitted telemetry.
"""
class NoOpLoggerProvider(LoggerProvider):
"""The default LoggerProvider used when no LoggerProvider implementation is available."""
def get_logger(
self,
name: str,
version: Optional[str] = None,
schema_url: Optional[str] = None,
) -> Logger:
"""Returns a NoOpLogger."""
super().get_logger(name, version=version, schema_url=schema_url)
return NoOpLogger(name, version=version, schema_url=schema_url)
# TODO: ProxyLoggerProvider
_LOGGER_PROVIDER_SET_ONCE = Once()
_LOGGER_PROVIDER = None
def get_logger_provider() -> LoggerProvider:
"""Gets the current global :class:`~.LoggerProvider` object."""
global _LOGGER_PROVIDER # pylint: disable=global-statement
if _LOGGER_PROVIDER is None:
if _OTEL_PYTHON_LOGGER_PROVIDER not in environ.keys():
# TODO: return proxy
_LOGGER_PROVIDER = NoOpLoggerProvider()
return _LOGGER_PROVIDER
logger_provider: LoggerProvider = _load_provider( # type: ignore
_OTEL_PYTHON_LOGGER_PROVIDER, "logger_provider"
)
_set_logger_provider(logger_provider, log=False)
# _LOGGER_PROVIDER will have been set by one thread
return cast("LoggerProvider", _LOGGER_PROVIDER)
def _set_logger_provider(logger_provider: LoggerProvider, log: bool) -> None:
def set_lp() -> None:
global _LOGGER_PROVIDER # pylint: disable=global-statement
_LOGGER_PROVIDER = logger_provider # type: ignore
did_set = _LOGGER_PROVIDER_SET_ONCE.do_once(set_lp)
if log and not did_set:
_logger.warning("Overriding of current LoggerProvider is not allowed")
def set_logger_provider(meter_provider: LoggerProvider) -> None:
"""Sets the current global :class:`~.LoggerProvider` object.
This can only be done once, a warning will be logged if any further attempt
is made.
"""
_set_logger_provider(meter_provider, log=True)
def get_logger(
instrumenting_module_name: str,
instrumenting_library_version: str = "",
logger_provider: Optional[LoggerProvider] = None,
) -> "Logger":
"""Returns a `Logger` for use within a python process.
This function is a convenience wrapper for
mysql.opentelemetry.sdk._logs.LoggerProvider.get_logger.
If logger_provider param is omitted the current configured one is used.
"""
if logger_provider is None:
logger_provider = get_logger_provider()
return logger_provider.get_logger(
instrumenting_module_name, instrumenting_library_version
)
@@ -0,0 +1,115 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import enum
class SeverityNumber(enum.Enum):
"""Numerical value of severity.
Smaller numerical values correspond to less severe events
(such as debug events), larger numerical values correspond
to more severe events (such as errors and critical events).
See the `Log Data Model`_ spec for more info and how to map the
severity from source format to OTLP Model.
.. _Log Data Model: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#field-severitynumber
"""
UNSPECIFIED = 0
TRACE = 1
TRACE2 = 2
TRACE3 = 3
TRACE4 = 4
DEBUG = 5
DEBUG2 = 6
DEBUG3 = 7
DEBUG4 = 8
INFO = 9
INFO2 = 10
INFO3 = 11
INFO4 = 12
WARN = 13
WARN2 = 14
WARN3 = 15
WARN4 = 16
ERROR = 17
ERROR2 = 18
ERROR3 = 19
ERROR4 = 20
FATAL = 21
FATAL2 = 22
FATAL3 = 23
FATAL4 = 24
_STD_TO_OTEL = {
10: SeverityNumber.DEBUG,
11: SeverityNumber.DEBUG2,
12: SeverityNumber.DEBUG3,
13: SeverityNumber.DEBUG4,
14: SeverityNumber.DEBUG4,
15: SeverityNumber.DEBUG4,
16: SeverityNumber.DEBUG4,
17: SeverityNumber.DEBUG4,
18: SeverityNumber.DEBUG4,
19: SeverityNumber.DEBUG4,
20: SeverityNumber.INFO,
21: SeverityNumber.INFO2,
22: SeverityNumber.INFO3,
23: SeverityNumber.INFO4,
24: SeverityNumber.INFO4,
25: SeverityNumber.INFO4,
26: SeverityNumber.INFO4,
27: SeverityNumber.INFO4,
28: SeverityNumber.INFO4,
29: SeverityNumber.INFO4,
30: SeverityNumber.WARN,
31: SeverityNumber.WARN2,
32: SeverityNumber.WARN3,
33: SeverityNumber.WARN4,
34: SeverityNumber.WARN4,
35: SeverityNumber.WARN4,
36: SeverityNumber.WARN4,
37: SeverityNumber.WARN4,
38: SeverityNumber.WARN4,
39: SeverityNumber.WARN4,
40: SeverityNumber.ERROR,
41: SeverityNumber.ERROR2,
42: SeverityNumber.ERROR3,
43: SeverityNumber.ERROR4,
44: SeverityNumber.ERROR4,
45: SeverityNumber.ERROR4,
46: SeverityNumber.ERROR4,
47: SeverityNumber.ERROR4,
48: SeverityNumber.ERROR4,
49: SeverityNumber.ERROR4,
50: SeverityNumber.FATAL,
51: SeverityNumber.FATAL2,
52: SeverityNumber.FATAL3,
53: SeverityNumber.FATAL4,
}
def std_to_otel(levelno: int) -> SeverityNumber:
"""
Map python log levelno as defined in https://docs.python.org/3/library/logging.html#logging-levels
to OTel log severity number as defined here: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#field-severitynumber
"""
if levelno < 10:
return SeverityNumber.UNSPECIFIED
if levelno > 53:
return SeverityNumber.FATAL4
return _STD_TO_OTEL[levelno]
+191
View File
@@ -0,0 +1,191 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# type: ignore
import logging
import threading
from collections import OrderedDict
from collections.abc import MutableMapping
from typing import Optional, Sequence, Union
from mysql.opentelemetry.util import types
# bytes are accepted as a user supplied value for attributes but
# decoded to strings internally.
_VALID_ATTR_VALUE_TYPES = (bool, str, bytes, int, float)
_logger = logging.getLogger(__name__)
def _clean_attribute(
key: str, value: types.AttributeValue, max_len: Optional[int]
) -> Optional[types.AttributeValue]:
"""Checks if attribute value is valid and cleans it if required.
The function returns the cleaned value or None if the value is not valid.
An attribute value is valid if it is either:
- A primitive type: string, boolean, double precision floating
point (IEEE 754-1985) or integer.
- An array of primitive type values. The array MUST be homogeneous,
i.e. it MUST NOT contain values of different types.
An attribute needs cleansing if:
- Its length is greater than the maximum allowed length.
- It needs to be encoded/decoded e.g, bytes to strings.
"""
if not (key and isinstance(key, str)):
_logger.warning("invalid key `%s`. must be non-empty string.", key)
return None
if isinstance(value, _VALID_ATTR_VALUE_TYPES):
return _clean_attribute_value(value, max_len)
if isinstance(value, Sequence):
sequence_first_valid_type = None
cleaned_seq = []
for element in value:
element = _clean_attribute_value(element, max_len)
if element is None:
cleaned_seq.append(element)
continue
element_type = type(element)
# Reject attribute value if sequence contains a value with an incompatible type.
if element_type not in _VALID_ATTR_VALUE_TYPES:
_logger.warning(
"Invalid type %s in attribute value sequence. Expected one of "
"%s or None",
element_type.__name__,
[valid_type.__name__ for valid_type in _VALID_ATTR_VALUE_TYPES],
)
return None
# The type of the sequence must be homogeneous. The first non-None
# element determines the type of the sequence
if sequence_first_valid_type is None:
sequence_first_valid_type = element_type
# use equality instead of isinstance as isinstance(True, int) evaluates to True
elif element_type != sequence_first_valid_type:
_logger.warning(
"Attribute %r mixes types %s and %s in attribute value sequence",
key,
sequence_first_valid_type.__name__,
type(element).__name__,
)
return None
cleaned_seq.append(element)
# Freeze mutable sequences defensively
return tuple(cleaned_seq)
_logger.warning(
"Invalid type %s for attribute '%s' value. Expected one of %s or a "
"sequence of those types",
type(value).__name__,
key,
[valid_type.__name__ for valid_type in _VALID_ATTR_VALUE_TYPES],
)
return None
def _clean_attribute_value(
value: types.AttributeValue, limit: Optional[int]
) -> Union[types.AttributeValue, None]:
if value is None:
return None
if isinstance(value, bytes):
try:
value = value.decode()
except UnicodeDecodeError:
_logger.warning("Byte attribute could not be decoded.")
return None
if limit is not None and isinstance(value, str):
value = value[:limit]
return value
class BoundedAttributes(MutableMapping):
"""An ordered dict with a fixed max capacity.
Oldest elements are dropped when the dict is full and a new element is
added.
"""
def __init__(
self,
maxlen: Optional[int] = None,
attributes: types.Attributes = None,
immutable: bool = True,
max_value_len: Optional[int] = None,
):
if maxlen is not None:
if not isinstance(maxlen, int) or maxlen < 0:
raise ValueError("maxlen must be valid int greater or equal to 0")
self.maxlen = maxlen
self.dropped = 0
self.max_value_len = max_value_len
self._dict = OrderedDict() # type: OrderedDict
self._lock = threading.Lock() # type: threading.Lock
if attributes:
for key, value in attributes.items():
self[key] = value
self._immutable = immutable
def __repr__(self):
return f"{type(self).__name__}({dict(self._dict)}, maxlen={self.maxlen})"
def __getitem__(self, key):
return self._dict[key]
def __setitem__(self, key, value):
if getattr(self, "_immutable", False):
raise TypeError
with self._lock:
if self.maxlen is not None and self.maxlen == 0:
self.dropped += 1
return
value = _clean_attribute(key, value, self.max_value_len)
if value is not None:
if key in self._dict:
del self._dict[key]
elif self.maxlen is not None and len(self._dict) == self.maxlen:
self._dict.popitem(last=False)
self.dropped += 1
self._dict[key] = value
def __delitem__(self, key):
if getattr(self, "_immutable", False):
raise TypeError
with self._lock:
del self._dict[key]
def __iter__(self):
with self._lock:
return iter(self._dict.copy())
def __len__(self):
return len(self._dict)
def copy(self):
return self._dict.copy()
+128
View File
@@ -0,0 +1,128 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from logging import getLogger
from re import compile
from types import MappingProxyType
from typing import Mapping, Optional
from mysql.opentelemetry.context import create_key, get_value, set_value
from mysql.opentelemetry.context.context import Context
from mysql.opentelemetry.util.re import (
_BAGGAGE_PROPERTY_FORMAT,
_KEY_FORMAT,
_VALUE_FORMAT,
)
_BAGGAGE_KEY = create_key("baggage")
_logger = getLogger(__name__)
_KEY_PATTERN = compile(_KEY_FORMAT)
_VALUE_PATTERN = compile(_VALUE_FORMAT)
_PROPERT_PATTERN = compile(_BAGGAGE_PROPERTY_FORMAT)
def get_all(
context: Optional[Context] = None,
) -> Mapping[str, object]:
"""Returns the name/value pairs in the Baggage
Args:
context: The Context to use. If not set, uses current Context
Returns:
The name/value pairs in the Baggage
"""
baggage = get_value(_BAGGAGE_KEY, context=context)
if isinstance(baggage, dict):
return MappingProxyType(baggage)
return MappingProxyType({})
def get_baggage(name: str, context: Optional[Context] = None) -> Optional[object]:
"""Provides access to the value for a name/value pair in the
Baggage
Args:
name: The name of the value to retrieve
context: The Context to use. If not set, uses current Context
Returns:
The value associated with the given name, or null if the given name is
not present.
"""
return get_all(context=context).get(name)
def set_baggage(name: str, value: object, context: Optional[Context] = None) -> Context:
"""Sets a value in the Baggage
Args:
name: The name of the value to set
value: The value to set
context: The Context to use. If not set, uses current Context
Returns:
A Context with the value updated
"""
baggage = dict(get_all(context=context))
baggage[name] = value
return set_value(_BAGGAGE_KEY, baggage, context=context)
def remove_baggage(name: str, context: Optional[Context] = None) -> Context:
"""Removes a value from the Baggage
Args:
name: The name of the value to remove
context: The Context to use. If not set, uses current Context
Returns:
A Context with the name/value removed
"""
baggage = dict(get_all(context=context))
baggage.pop(name, None)
return set_value(_BAGGAGE_KEY, baggage, context=context)
def clear(context: Optional[Context] = None) -> Context:
"""Removes all values from the Baggage
Args:
context: The Context to use. If not set, uses current Context
Returns:
A Context with all baggage entries removed
"""
return set_value(_BAGGAGE_KEY, {}, context=context)
def _is_valid_key(name: str) -> bool:
return _KEY_PATTERN.fullmatch(str(name)) is not None
def _is_valid_value(value: object) -> bool:
parts = str(value).split(";")
is_valid_value = _VALUE_PATTERN.fullmatch(parts[0]) is not None
if len(parts) > 1: # one or more properties metadata
for property in parts[1:]:
if _PROPERT_PATTERN.fullmatch(property) is None:
is_valid_value = False
break
return is_valid_value
def _is_valid_pair(key: str, value: str) -> bool:
return _is_valid_key(key) and _is_valid_value(value)
@@ -0,0 +1,144 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from logging import getLogger
from re import split
from typing import Iterable, List, Mapping, Optional, Set
from urllib.parse import quote_plus, unquote_plus
from mysql.opentelemetry.baggage import _is_valid_pair, get_all, set_baggage
from mysql.opentelemetry.context import get_current
from mysql.opentelemetry.context.context import Context
from mysql.opentelemetry.propagators import textmap
from mysql.opentelemetry.util.re import _DELIMITER_PATTERN
_logger = getLogger(__name__)
class W3CBaggagePropagator(textmap.TextMapPropagator):
"""Extracts and injects Baggage which is used to annotate telemetry."""
_MAX_HEADER_LENGTH = 8192
_MAX_PAIR_LENGTH = 4096
_MAX_PAIRS = 180
_BAGGAGE_HEADER_NAME = "baggage"
def extract(
self,
carrier: textmap.CarrierT,
context: Optional[Context] = None,
getter: textmap.Getter[textmap.CarrierT] = textmap.default_getter,
) -> Context:
"""Extract Baggage from the carrier.
See
`mysql.opentelemetry.propagators.textmap.TextMapPropagator.extract`
"""
if context is None:
context = get_current()
header = _extract_first_element(getter.get(carrier, self._BAGGAGE_HEADER_NAME))
if not header:
return context
if len(header) > self._MAX_HEADER_LENGTH:
_logger.warning(
"Baggage header `%s` exceeded the maximum number of bytes per baggage-string",
header,
)
return context
baggage_entries: List[str] = split(_DELIMITER_PATTERN, header)
total_baggage_entries = self._MAX_PAIRS
if len(baggage_entries) > self._MAX_PAIRS:
_logger.warning(
"Baggage header `%s` exceeded the maximum number of list-members",
header,
)
for entry in baggage_entries:
if len(entry) > self._MAX_PAIR_LENGTH:
_logger.warning(
"Baggage entry `%s` exceeded the maximum number of bytes per list-member",
entry,
)
continue
if not entry: # empty string
continue
try:
name, value = entry.split("=", 1)
except Exception: # pylint: disable=broad-except
_logger.warning(
"Baggage list-member `%s` doesn't match the format", entry
)
continue
if not _is_valid_pair(name, value):
_logger.warning("Invalid baggage entry: `%s`", entry)
continue
name = unquote_plus(name).strip()
value = unquote_plus(value).strip()
context = set_baggage(
name,
value,
context=context,
)
total_baggage_entries -= 1
if total_baggage_entries == 0:
break
return context
def inject(
self,
carrier: textmap.CarrierT,
context: Optional[Context] = None,
setter: textmap.Setter[textmap.CarrierT] = textmap.default_setter,
) -> None:
"""Injects Baggage into the carrier.
See
`mysql.opentelemetry.propagators.textmap.TextMapPropagator.inject`
"""
baggage_entries = get_all(context=context)
if not baggage_entries:
return
baggage_string = _format_baggage(baggage_entries)
setter.set(carrier, self._BAGGAGE_HEADER_NAME, baggage_string)
@property
def fields(self) -> Set[str]:
"""Returns a set with the fields set in `inject`."""
return {self._BAGGAGE_HEADER_NAME}
def _format_baggage(baggage_entries: Mapping[str, object]) -> str:
return ",".join(
quote_plus(str(key)) + "=" + quote_plus(str(value))
for key, value in baggage_entries.items()
)
def _extract_first_element(
items: Optional[Iterable[textmap.CarrierT]],
) -> Optional[textmap.CarrierT]:
if items is None:
return None
return next(iter(items), None)
+170
View File
@@ -0,0 +1,170 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import threading
import typing
from functools import wraps
from os import environ
from uuid import uuid4
# pylint: disable=wrong-import-position
from mysql.opentelemetry.context.context import Context, _RuntimeContext # noqa
from mysql.opentelemetry.environment_variables import OTEL_PYTHON_CONTEXT
from mysql.opentelemetry.util._importlib_metadata import entry_points
logger = logging.getLogger(__name__)
_RUNTIME_CONTEXT = None # type: typing.Optional[_RuntimeContext]
_RUNTIME_CONTEXT_LOCK = threading.Lock()
_F = typing.TypeVar("_F", bound=typing.Callable[..., typing.Any])
def _load_runtime_context(func: _F) -> _F:
"""A decorator used to initialize the global RuntimeContext
Returns:
A wrapper of the decorated method.
"""
@wraps(func) # type: ignore[misc]
def wrapper( # type: ignore[misc]
*args: typing.Tuple[typing.Any, typing.Any],
**kwargs: typing.Dict[typing.Any, typing.Any],
) -> typing.Optional[typing.Any]:
global _RUNTIME_CONTEXT # pylint: disable=global-statement
with _RUNTIME_CONTEXT_LOCK:
if _RUNTIME_CONTEXT is None:
# FIXME use a better implementation of a configuration manager
# to avoid having to get configuration values straight from
# environment variables
default_context = "contextvars_context"
configured_context = environ.get(
OTEL_PYTHON_CONTEXT, default_context
) # type: str
try:
_RUNTIME_CONTEXT = next( # type: ignore
iter( # type: ignore
entry_points( # type: ignore
group="opentelemetry_context",
name=configured_context,
)
)
).load()()
except Exception: # pylint: disable=broad-except
logger.exception("Failed to load context: %s", configured_context)
return func(*args, **kwargs) # type: ignore[misc]
return typing.cast(_F, wrapper) # type: ignore[misc]
def create_key(keyname: str) -> str:
"""To allow cross-cutting concern to control access to their local state,
the RuntimeContext API provides a function which takes a keyname as input,
and returns a unique key.
Args:
keyname: The key name is for debugging purposes and is not required to be unique.
Returns:
A unique string representing the newly created key.
"""
return keyname + "-" + str(uuid4())
def get_value(key: str, context: typing.Optional[Context] = None) -> "object":
"""To access the local state of a concern, the RuntimeContext API
provides a function which takes a context and a key as input,
and returns a value.
Args:
key: The key of the value to retrieve.
context: The context from which to retrieve the value, if None, the current context is used.
Returns:
The value associated with the key.
"""
return context.get(key) if context is not None else get_current().get(key)
def set_value(
key: str, value: "object", context: typing.Optional[Context] = None
) -> Context:
"""To record the local state of a cross-cutting concern, the
RuntimeContext API provides a function which takes a context, a
key, and a value as input, and returns an updated context
which contains the new value.
Args:
key: The key of the entry to set.
value: The value of the entry to set.
context: The context to copy, if None, the current context is used.
Returns:
A new `Context` containing the value set.
"""
if context is None:
context = get_current()
new_values = context.copy()
new_values[key] = value
return Context(new_values)
@_load_runtime_context # type: ignore
def get_current() -> Context:
"""To access the context associated with program execution,
the Context API provides a function which takes no arguments
and returns a Context.
Returns:
The current `Context` object.
"""
return _RUNTIME_CONTEXT.get_current() # type:ignore
@_load_runtime_context # type: ignore
def attach(context: Context) -> object:
"""Associates a Context with the caller's current execution unit. Returns
a token that can be used to restore the previous Context.
Args:
context: The Context to set as current.
Returns:
A token that can be used with `detach` to reset the context.
"""
return _RUNTIME_CONTEXT.attach(context) # type:ignore
@_load_runtime_context # type: ignore
def detach(token: object) -> None:
"""Resets the Context associated with the caller's current execution unit
to the value it had before attaching a specified Context.
Args:
token: The Token that was returned by a previous call to attach a Context.
"""
try:
_RUNTIME_CONTEXT.detach(token) # type: ignore
except Exception: # pylint: disable=broad-except
logger.exception("Failed to detach context")
# FIXME This is a temporary location for the suppress instrumentation key.
# Once the decision around how to suppress instrumentation is made in the
# spec, this key should be moved accordingly.
_SUPPRESS_INSTRUMENTATION_KEY = create_key("suppress_instrumentation")
_SUPPRESS_HTTP_INSTRUMENTATION_KEY = create_key("suppress_http_instrumentation")
+54
View File
@@ -0,0 +1,54 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import typing
from abc import ABC, abstractmethod
class Context(typing.Dict[str, object]):
def __setitem__(self, key: str, value: object) -> None:
raise ValueError
class _RuntimeContext(ABC):
"""The RuntimeContext interface provides a wrapper for the different
mechanisms that are used to propagate context in Python.
Implementations can be made available via entry_points and
selected through environment variables.
"""
@abstractmethod
def attach(self, context: Context) -> object:
"""Sets the current `Context` object. Returns a
token that can be used to reset to the previous `Context`.
Args:
context: The Context to set.
"""
@abstractmethod
def get_current(self) -> Context:
"""Returns the current `Context` object."""
@abstractmethod
def detach(self, token: object) -> None:
"""Resets Context to a previous value
Args:
token: A reference to a previous Context.
"""
__all__ = ["Context"]
@@ -0,0 +1,51 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from contextvars import ContextVar
from mysql.opentelemetry.context.context import Context, _RuntimeContext
class ContextVarsRuntimeContext(_RuntimeContext):
"""An implementation of the RuntimeContext interface which wraps ContextVar under
the hood. This is the preferred implementation for usage with Python 3.5+
"""
_CONTEXT_KEY = "current_context"
def __init__(self) -> None:
self._current_context = ContextVar(self._CONTEXT_KEY, default=Context())
def attach(self, context: Context) -> object:
"""Sets the current `Context` object. Returns a
token that can be used to reset to the previous `Context`.
Args:
context: The Context to set.
"""
return self._current_context.set(context)
def get_current(self) -> Context:
"""Returns the current `Context` object."""
return self._current_context.get()
def detach(self, token: object) -> None:
"""Resets Context to a previous value
Args:
token: A reference to a previous Context.
"""
self._current_context.reset(token) # type: ignore
__all__ = ["ContextVarsRuntimeContext"]
@@ -0,0 +1,60 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
OTEL_LOGS_EXPORTER = "OTEL_LOGS_EXPORTER"
"""
.. envvar:: OTEL_LOGS_EXPORTER
"""
OTEL_METRICS_EXPORTER = "OTEL_METRICS_EXPORTER"
"""
.. envvar:: OTEL_METRICS_EXPORTER
"""
OTEL_PROPAGATORS = "OTEL_PROPAGATORS"
"""
.. envvar:: OTEL_PROPAGATORS
"""
OTEL_PYTHON_CONTEXT = "OTEL_PYTHON_CONTEXT"
"""
.. envvar:: OTEL_PYTHON_CONTEXT
"""
OTEL_PYTHON_ID_GENERATOR = "OTEL_PYTHON_ID_GENERATOR"
"""
.. envvar:: OTEL_PYTHON_ID_GENERATOR
"""
OTEL_TRACES_EXPORTER = "OTEL_TRACES_EXPORTER"
"""
.. envvar:: OTEL_TRACES_EXPORTER
"""
OTEL_PYTHON_TRACER_PROVIDER = "OTEL_PYTHON_TRACER_PROVIDER"
"""
.. envvar:: OTEL_PYTHON_TRACER_PROVIDER
"""
OTEL_PYTHON_METER_PROVIDER = "OTEL_PYTHON_METER_PROVIDER"
"""
.. envvar:: OTEL_PYTHON_METER_PROVIDER
"""
_OTEL_PYTHON_LOGGER_PROVIDER = "OTEL_PYTHON_LOGGER_PROVIDER"
"""
.. envvar:: OTEL_PYTHON_LOGGER_PROVIDER
"""
@@ -0,0 +1,983 @@
import abc
import collections
import contextlib
import csv
import email
import functools
import inspect
import itertools
import operator
import os
import pathlib
import posixpath
import re
import sys
import textwrap
import warnings
from contextlib import suppress
from importlib import import_module
from importlib.abc import MetaPathFinder
from itertools import starmap
from typing import Iterable, List, Mapping, Optional, Set, cast
import zipp
from . import _adapters, _meta, _py39compat
from ._collections import FreezableDefaultDict, Pair
from ._compat import NullFinder, StrPath, install, pypy_partial
from ._functools import method_cache, pass_none
from ._itertools import always_iterable, unique_everseen
from ._meta import PackageMetadata, SimplePath
__all__ = [
"Distribution",
"DistributionFinder",
"PackageMetadata",
"PackageNotFoundError",
"distribution",
"distributions",
"entry_points",
"files",
"metadata",
"packages_distributions",
"requires",
"version",
]
class PackageNotFoundError(ModuleNotFoundError):
"""The package was not found."""
def __str__(self) -> str:
return f"No package metadata was found for {self.name}"
@property
def name(self) -> str: # type: ignore[override]
(name,) = self.args
return name
class Sectioned:
"""
A simple entry point config parser for performance
>>> for item in Sectioned.read(Sectioned._sample):
... print(item)
Pair(name='sec1', value='# comments ignored')
Pair(name='sec1', value='a = 1')
Pair(name='sec1', value='b = 2')
Pair(name='sec2', value='a = 2')
>>> res = Sectioned.section_pairs(Sectioned._sample)
>>> item = next(res)
>>> item.name
'sec1'
>>> item.value
Pair(name='a', value='1')
>>> item = next(res)
>>> item.value
Pair(name='b', value='2')
>>> item = next(res)
>>> item.name
'sec2'
>>> item.value
Pair(name='a', value='2')
>>> list(res)
[]
"""
_sample = textwrap.dedent(
"""
[sec1]
# comments ignored
a = 1
b = 2
[sec2]
a = 2
"""
).lstrip()
@classmethod
def section_pairs(cls, text):
return (
section._replace(value=Pair.parse(section.value))
for section in cls.read(text, filter_=cls.valid)
if section.name is not None
)
@staticmethod
def read(text, filter_=None):
lines = filter(filter_, map(str.strip, text.splitlines()))
name = None
for value in lines:
section_match = value.startswith("[") and value.endswith("]")
if section_match:
name = value.strip("[]")
continue
yield Pair(name, value)
@staticmethod
def valid(line: str):
return line and not line.startswith("#")
class DeprecatedTuple:
"""
Provide subscript item access for backward compatibility.
>>> recwarn = getfixture('recwarn')
>>> ep = EntryPoint(name='name', value='value', group='group')
>>> ep[:]
('name', 'value', 'group')
>>> ep[0]
'name'
>>> len(recwarn)
1
"""
# Do not remove prior to 2023-05-01 or Python 3.13
_warn = functools.partial(
warnings.warn,
"EntryPoint tuple interface is deprecated. Access members by name.",
DeprecationWarning,
stacklevel=pypy_partial(2),
)
def __getitem__(self, item):
self._warn()
return self._key()[item]
class EntryPoint(DeprecatedTuple):
"""An entry point as defined by Python packaging conventions.
See `the packaging docs on entry points
<https://packaging.python.org/specifications/entry-points/>`_
for more information.
>>> ep = EntryPoint(
... name=None, group=None, value='package.module:attr [extra1, extra2]')
>>> ep.module
'package.module'
>>> ep.attr
'attr'
>>> ep.extras
['extra1', 'extra2']
"""
pattern = re.compile(
r"(?P<module>[\w.]+)\s*"
r"(:\s*(?P<attr>[\w.]+)\s*)?"
r"((?P<extras>\[.*\])\s*)?$"
)
"""
A regular expression describing the syntax for an entry point,
which might look like:
- module
- package.module
- package.module:attribute
- package.module:object.attribute
- package.module:attr [extra1, extra2]
Other combinations are possible as well.
The expression is lenient about whitespace around the ':',
following the attr, and following any extras.
"""
name: str
value: str
group: str
dist: Optional["Distribution"] = None
def __init__(self, name: str, value: str, group: str) -> None:
vars(self).update(name=name, value=value, group=group)
def load(self):
"""Load the entry point from its definition. If only a module
is indicated by the value, return that module. Otherwise,
return the named object.
"""
match = self.pattern.match(self.value)
module = import_module(match.group("module"))
attrs = filter(None, (match.group("attr") or "").split("."))
return functools.reduce(getattr, attrs, module)
@property
def module(self) -> str:
match = self.pattern.match(self.value)
assert match is not None
return match.group("module")
@property
def attr(self) -> str:
match = self.pattern.match(self.value)
assert match is not None
return match.group("attr")
@property
def extras(self) -> List[str]:
match = self.pattern.match(self.value)
assert match is not None
return re.findall(r"\w+", match.group("extras") or "")
def _for(self, dist):
vars(self).update(dist=dist)
return self
def matches(self, **params):
"""
EntryPoint matches the given parameters.
>>> ep = EntryPoint(group='foo', name='bar', value='bing:bong [extra1, extra2]')
>>> ep.matches(group='foo')
True
>>> ep.matches(name='bar', value='bing:bong [extra1, extra2]')
True
>>> ep.matches(group='foo', name='other')
False
>>> ep.matches()
True
>>> ep.matches(extras=['extra1', 'extra2'])
True
>>> ep.matches(module='bing')
True
>>> ep.matches(attr='bong')
True
"""
attrs = (getattr(self, param) for param in params)
return all(map(operator.eq, params.values(), attrs))
def _key(self):
return self.name, self.value, self.group
def __lt__(self, other):
return self._key() < other._key()
def __eq__(self, other):
return self._key() == other._key()
def __setattr__(self, name, value):
raise AttributeError("EntryPoint objects are immutable.")
def __repr__(self):
return (
f"EntryPoint(name={self.name!r}, value={self.value!r}, "
f"group={self.group!r})"
)
def __hash__(self) -> int:
return hash(self._key())
class EntryPoints(tuple):
"""
An immutable collection of selectable EntryPoint objects.
"""
__slots__ = ()
def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override]
"""
Get the EntryPoint in self matching name.
"""
try:
return next(iter(self.select(name=name)))
except StopIteration:
raise KeyError(name)
def select(self, **params):
"""
Select entry points from self that match the
given parameters (typically group and/or name).
"""
return EntryPoints(ep for ep in self if _py39compat.ep_matches(ep, **params))
@property
def names(self) -> Set[str]:
"""
Return the set of all names of all entry points.
"""
return {ep.name for ep in self}
@property
def groups(self) -> Set[str]:
"""
Return the set of all groups of all entry points.
"""
return {ep.group for ep in self}
@classmethod
def _from_text_for(cls, text, dist):
return cls(ep._for(dist) for ep in cls._from_text(text))
@staticmethod
def _from_text(text):
return (
EntryPoint(name=item.value.name, value=item.value.value, group=item.name)
for item in Sectioned.section_pairs(text or "")
)
class PackagePath(pathlib.PurePosixPath):
"""A reference to a path in a package"""
hash: Optional["FileHash"]
size: int
dist: "Distribution"
def read_text(self, encoding: str = "utf-8") -> str: # type: ignore[override]
with self.locate().open(encoding=encoding) as stream:
return stream.read()
def read_binary(self) -> bytes:
with self.locate().open("rb") as stream:
return stream.read()
def locate(self) -> pathlib.Path:
"""Return a path-like object for this path"""
return self.dist.locate_file(self)
class FileHash:
def __init__(self, spec: str) -> None:
self.mode, _, self.value = spec.partition("=")
def __repr__(self) -> str:
return f"<FileHash mode: {self.mode} value: {self.value}>"
class DeprecatedNonAbstract:
def __new__(cls, *args, **kwargs):
all_names = {
name for subclass in inspect.getmro(cls) for name in vars(subclass)
}
abstract = {
name
for name in all_names
if getattr(getattr(cls, name), "__isabstractmethod__", False)
}
if abstract:
warnings.warn(
f"Unimplemented abstract methods {abstract}",
DeprecationWarning,
stacklevel=2,
)
return super().__new__(cls)
class Distribution(DeprecatedNonAbstract):
"""A Python distribution package."""
@abc.abstractmethod
def read_text(self, filename) -> Optional[str]:
"""Attempt to load metadata file given by the name.
:param filename: The name of the file in the distribution info.
:return: The text if found, otherwise None.
"""
@abc.abstractmethod
def locate_file(self, path: StrPath) -> pathlib.Path:
"""
Given a path to a file in this distribution, return a path
to it.
"""
@classmethod
def from_name(cls, name: str) -> "Distribution":
"""Return the Distribution for the given package name.
:param name: The name of the distribution package to search for.
:return: The Distribution instance (or subclass thereof) for the named
package, if found.
:raises PackageNotFoundError: When the named package's distribution
metadata cannot be found.
:raises ValueError: When an invalid value is supplied for name.
"""
if not name:
raise ValueError("A distribution name is required.")
try:
return next(iter(cls.discover(name=name)))
except StopIteration:
raise PackageNotFoundError(name)
@classmethod
def discover(cls, **kwargs) -> Iterable["Distribution"]:
"""Return an iterable of Distribution objects for all packages.
Pass a ``context`` or pass keyword arguments for constructing
a context.
:context: A ``DistributionFinder.Context`` object.
:return: Iterable of Distribution objects for all packages.
"""
context = kwargs.pop("context", None)
if context and kwargs:
raise ValueError("cannot accept context and kwargs")
context = context or DistributionFinder.Context(**kwargs)
return itertools.chain.from_iterable(
resolver(context) for resolver in cls._discover_resolvers()
)
@staticmethod
def at(path: StrPath) -> "Distribution":
"""Return a Distribution for the indicated metadata path
:param path: a string or path-like object
:return: a concrete Distribution instance for the path
"""
return PathDistribution(pathlib.Path(path))
@staticmethod
def _discover_resolvers():
"""Search the meta_path for resolvers."""
declared = (
getattr(finder, "find_distributions", None) for finder in sys.meta_path
)
return filter(None, declared)
@property
def metadata(self) -> _meta.PackageMetadata:
"""Return the parsed metadata for this Distribution.
The returned object will have keys that name the various bits of
metadata. See PEP 566 for details.
"""
opt_text = (
self.read_text("METADATA")
or self.read_text("PKG-INFO")
# This last clause is here to support old egg-info files. Its
# effect is to just end up using the PathDistribution's self._path
# (which points to the egg-info file) attribute unchanged.
or self.read_text("")
)
text = cast(str, opt_text)
return _adapters.Message(email.message_from_string(text))
@property
def name(self) -> str:
"""Return the 'Name' metadata for the distribution package."""
return self.metadata["Name"]
@property
def _normalized_name(self):
"""Return a normalized version of the name."""
return Prepared.normalize(self.name)
@property
def version(self) -> str:
"""Return the 'Version' metadata for the distribution package."""
return self.metadata["Version"]
@property
def entry_points(self) -> EntryPoints:
return EntryPoints._from_text_for(self.read_text("entry_points.txt"), self)
@property
def files(self) -> Optional[List[PackagePath]]:
"""Files in this distribution.
:return: List of PackagePath for this distribution or None
Result is `None` if the metadata file that enumerates files
(i.e. RECORD for dist-info, or installed-files.txt or
SOURCES.txt for egg-info) is missing.
Result may be empty if the metadata exists but is empty.
"""
def make_file(name, hash=None, size_str=None):
result = PackagePath(name)
result.hash = FileHash(hash) if hash else None
result.size = int(size_str) if size_str else None
result.dist = self
return result
@pass_none
def make_files(lines):
return starmap(make_file, csv.reader(lines))
@pass_none
def skip_missing_files(package_paths):
return list(filter(lambda path: path.locate().exists(), package_paths))
return skip_missing_files(
make_files(
self._read_files_distinfo()
or self._read_files_egginfo_installed()
or self._read_files_egginfo_sources()
)
)
def _read_files_distinfo(self):
"""
Read the lines of RECORD
"""
text = self.read_text("RECORD")
return text and text.splitlines()
def _read_files_egginfo_installed(self):
"""
Read installed-files.txt and return lines in a similar
CSV-parsable format as RECORD: each file must be placed
relative to the site-packages directory and must also be
quoted (since file names can contain literal commas).
This file is written when the package is installed by pip,
but it might not be written for other installation methods.
Assume the file is accurate if it exists.
"""
text = self.read_text("installed-files.txt")
# Prepend the .egg-info/ subdir to the lines in this file.
# But this subdir is only available from PathDistribution's
# self._path.
subdir = getattr(self, "_path", None)
if not text or not subdir:
return
paths = (
(subdir / name)
.resolve()
.relative_to(self.locate_file("").resolve())
.as_posix()
for name in text.splitlines()
)
return map('"{}"'.format, paths)
def _read_files_egginfo_sources(self):
"""
Read SOURCES.txt and return lines in a similar CSV-parsable
format as RECORD: each file name must be quoted (since it
might contain literal commas).
Note that SOURCES.txt is not a reliable source for what
files are installed by a package. This file is generated
for a source archive, and the files that are present
there (e.g. setup.py) may not correctly reflect the files
that are present after the package has been installed.
"""
text = self.read_text("SOURCES.txt")
return text and map('"{}"'.format, text.splitlines())
@property
def requires(self) -> Optional[List[str]]:
"""Generated requirements specified for this Distribution"""
reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs()
return reqs and list(reqs)
def _read_dist_info_reqs(self):
return self.metadata.get_all("Requires-Dist")
def _read_egg_info_reqs(self):
source = self.read_text("requires.txt")
return pass_none(self._deps_from_requires_text)(source)
@classmethod
def _deps_from_requires_text(cls, source):
return cls._convert_egg_info_reqs_to_simple_reqs(Sectioned.read(source))
@staticmethod
def _convert_egg_info_reqs_to_simple_reqs(sections):
"""
Historically, setuptools would solicit and store 'extra'
requirements, including those with environment markers,
in separate sections. More modern tools expect each
dependency to be defined separately, with any relevant
extras and environment markers attached directly to that
requirement. This method converts the former to the
latter. See _test_deps_from_requires_text for an example.
"""
def make_condition(name):
return name and f'extra == "{name}"'
def quoted_marker(section):
section = section or ""
extra, sep, markers = section.partition(":")
if extra and markers:
markers = f"({markers})"
conditions = list(filter(None, [markers, make_condition(extra)]))
return "; " + " and ".join(conditions) if conditions else ""
def url_req_space(req):
"""
PEP 508 requires a space between the url_spec and the quoted_marker.
Ref python/importlib_metadata#357.
"""
# '@' is uniquely indicative of a url_req.
return " " * ("@" in req)
for section in sections:
space = url_req_space(section.value)
yield section.value + space + quoted_marker(section.name)
class DistributionFinder(MetaPathFinder):
"""
A MetaPathFinder capable of discovering installed distributions.
"""
class Context:
"""
Keyword arguments presented by the caller to
``distributions()`` or ``Distribution.discover()``
to narrow the scope of a search for distributions
in all DistributionFinders.
Each DistributionFinder may expect any parameters
and should attempt to honor the canonical
parameters defined below when appropriate.
"""
name = None
"""
Specific name for which a distribution finder should match.
A name of ``None`` matches all distributions.
"""
def __init__(self, **kwargs):
vars(self).update(kwargs)
@property
def path(self) -> List[str]:
"""
The sequence of directory path that a distribution finder
should search.
Typically refers to Python installed package paths such as
"site-packages" directories and defaults to ``sys.path``.
"""
return vars(self).get("path", sys.path)
@abc.abstractmethod
def find_distributions(self, context=Context()) -> Iterable[Distribution]:
"""
Find distributions.
Return an iterable of all Distribution instances capable of
loading the metadata for packages matching the ``context``,
a DistributionFinder.Context instance.
"""
class FastPath:
"""
Micro-optimized class for searching a path for
children.
>>> FastPath('').children()
['...']
"""
@functools.lru_cache() # type: ignore
def __new__(cls, root):
return super().__new__(cls)
def __init__(self, root):
self.root = root
def joinpath(self, child):
return pathlib.Path(self.root, child)
def children(self):
with suppress(Exception):
return os.listdir(self.root or ".")
with suppress(Exception):
return self.zip_children()
return []
def zip_children(self):
zip_path = zipp.Path(self.root)
names = zip_path.root.namelist()
self.joinpath = zip_path.joinpath
return dict.fromkeys(child.split(posixpath.sep, 1)[0] for child in names)
def search(self, name):
return self.lookup(self.mtime).search(name)
@property
def mtime(self):
with suppress(OSError):
return os.stat(self.root).st_mtime
self.lookup.cache_clear()
@method_cache
def lookup(self, mtime):
return Lookup(self)
class Lookup:
def __init__(self, path: FastPath):
base = os.path.basename(path.root).lower()
base_is_egg = base.endswith(".egg")
self.infos = FreezableDefaultDict(list)
self.eggs = FreezableDefaultDict(list)
for child in path.children():
low = child.lower()
if low.endswith((".dist-info", ".egg-info")):
# rpartition is faster than splitext and suitable for this purpose.
name = low.rpartition(".")[0].partition("-")[0]
normalized = Prepared.normalize(name)
self.infos[normalized].append(path.joinpath(child))
elif base_is_egg and low == "egg-info":
name = base.rpartition(".")[0].partition("-")[0]
legacy_normalized = Prepared.legacy_normalize(name)
self.eggs[legacy_normalized].append(path.joinpath(child))
self.infos.freeze()
self.eggs.freeze()
def search(self, prepared):
infos = (
self.infos[prepared.normalized]
if prepared
else itertools.chain.from_iterable(self.infos.values())
)
eggs = (
self.eggs[prepared.legacy_normalized]
if prepared
else itertools.chain.from_iterable(self.eggs.values())
)
return itertools.chain(infos, eggs)
class Prepared:
"""
A prepared search for metadata on a possibly-named package.
"""
normalized = None
legacy_normalized = None
def __init__(self, name):
self.name = name
if name is None:
return
self.normalized = self.normalize(name)
self.legacy_normalized = self.legacy_normalize(name)
@staticmethod
def normalize(name):
"""
PEP 503 normalization plus dashes as underscores.
"""
return re.sub(r"[-_.]+", "-", name).lower().replace("-", "_")
@staticmethod
def legacy_normalize(name):
"""
Normalize the package name as found in the convention in
older packaging tools versions and specs.
"""
return name.lower().replace("-", "_")
def __bool__(self):
return bool(self.name)
@install
class MetadataPathFinder(NullFinder, DistributionFinder):
"""A degenerate finder for distribution packages on the file system.
This finder supplies only a find_distributions() method for versions
of Python that do not have a PathFinder find_distributions().
"""
def find_distributions(
self, context=DistributionFinder.Context()
) -> Iterable["PathDistribution"]:
"""
Find distributions.
Return an iterable of all Distribution instances capable of
loading the metadata for packages matching ``context.name``
(or all names if ``None`` indicated) along the paths in the list
of directories ``context.path``.
"""
found = self._search_paths(context.name, context.path)
return map(PathDistribution, found)
@classmethod
def _search_paths(cls, name, paths):
"""Find metadata directories in paths heuristically."""
prepared = Prepared(name)
return itertools.chain.from_iterable(
path.search(prepared) for path in map(FastPath, paths)
)
def invalidate_caches(cls) -> None:
FastPath.__new__.cache_clear()
class PathDistribution(Distribution):
def __init__(self, path: SimplePath) -> None:
"""Construct a distribution.
:param path: SimplePath indicating the metadata directory.
"""
self._path = path
def read_text(self, filename: StrPath) -> Optional[str]:
with suppress(
FileNotFoundError,
IsADirectoryError,
KeyError,
NotADirectoryError,
PermissionError,
):
return self._path.joinpath(filename).read_text(encoding="utf-8")
return None
read_text.__doc__ = Distribution.read_text.__doc__
def locate_file(self, path: StrPath) -> pathlib.Path:
return self._path.parent / path
@property
def _normalized_name(self):
"""
Performance optimization: where possible, resolve the
normalized name from the file system path.
"""
stem = os.path.basename(str(self._path))
return (
pass_none(Prepared.normalize)(self._name_from_stem(stem))
or super()._normalized_name
)
@staticmethod
def _name_from_stem(stem):
"""
>>> PathDistribution._name_from_stem('foo-3.0.egg-info')
'foo'
>>> PathDistribution._name_from_stem('CherryPy-3.0.dist-info')
'CherryPy'
>>> PathDistribution._name_from_stem('face.egg-info')
'face'
>>> PathDistribution._name_from_stem('foo.bar')
"""
filename, ext = os.path.splitext(stem)
if ext not in (".dist-info", ".egg-info"):
return
name, sep, rest = filename.partition("-")
return name
def distribution(distribution_name) -> Distribution:
"""Get the ``Distribution`` instance for the named package.
:param distribution_name: The name of the distribution package as a string.
:return: A ``Distribution`` instance (or subclass thereof).
"""
return Distribution.from_name(distribution_name)
def distributions(**kwargs) -> Iterable[Distribution]:
"""Get all ``Distribution`` instances in the current environment.
:return: An iterable of ``Distribution`` instances.
"""
return Distribution.discover(**kwargs)
def metadata(distribution_name) -> _meta.PackageMetadata:
"""Get the metadata for the named package.
:param distribution_name: The name of the distribution package to query.
:return: A PackageMetadata containing the parsed metadata.
"""
return Distribution.from_name(distribution_name).metadata
def version(distribution_name) -> str:
"""Get the version string for the named package.
:param distribution_name: The name of the distribution package to query.
:return: The version string for the package as defined in the package's
"Version" metadata key.
"""
return distribution(distribution_name).version
_unique = functools.partial(
unique_everseen,
key=_py39compat.normalized_name,
)
"""
Wrapper for ``distributions`` to return unique distributions by name.
"""
def entry_points(**params) -> EntryPoints:
"""Return EntryPoint objects for all installed packages.
Pass selection parameters (group or name) to filter the
result to entry points matching those properties (see
EntryPoints.select()).
:return: EntryPoints for all installed packages.
"""
eps = itertools.chain.from_iterable(
dist.entry_points for dist in _unique(distributions())
)
return EntryPoints(eps).select(**params)
def files(distribution_name) -> Optional[List[PackagePath]]:
"""Return a list of files for the named package.
:param distribution_name: The name of the distribution package to query.
:return: List of files composing the distribution.
"""
return distribution(distribution_name).files
def requires(distribution_name) -> Optional[List[str]]:
"""
Return a list of requirements for the named package.
:return: An iterable of requirements, suitable for
packaging.requirement.Requirement.
"""
return distribution(distribution_name).requires
def packages_distributions() -> Mapping[str, List[str]]:
"""
Return a mapping of top-level packages to their
distributions.
>>> import collections.abc
>>> pkgs = packages_distributions()
>>> all(isinstance(dist, collections.abc.Sequence) for dist in pkgs.values())
True
"""
pkg_to_dist = collections.defaultdict(list)
for dist in distributions():
for pkg in _top_level_declared(dist) or _top_level_inferred(dist):
pkg_to_dist[pkg].append(dist.metadata["Name"])
return dict(pkg_to_dist)
def _top_level_declared(dist):
return (dist.read_text("top_level.txt") or "").split()
def _top_level_inferred(dist):
opt_names = {
f.parts[0] if len(f.parts) > 1 else inspect.getmodulename(f)
for f in always_iterable(dist.files)
}
@pass_none
def importable_name(name):
return "." not in name
return filter(importable_name, opt_names)
@@ -0,0 +1,89 @@
import email.message
import functools
import re
import textwrap
import warnings
from ._compat import pypy_partial
from ._text import FoldedCase
# Do not remove prior to 2024-01-01 or Python 3.14
_warn = functools.partial(
warnings.warn,
"Implicit None on return values is deprecated and will raise KeyErrors.",
DeprecationWarning,
stacklevel=pypy_partial(2),
)
class Message(email.message.Message):
multiple_use_keys = set(
map(
FoldedCase,
[
"Classifier",
"Obsoletes-Dist",
"Platform",
"Project-URL",
"Provides-Dist",
"Provides-Extra",
"Requires-Dist",
"Requires-External",
"Supported-Platform",
"Dynamic",
],
)
)
"""
Keys that may be indicated multiple times per PEP 566.
"""
def __new__(cls, orig: email.message.Message):
res = super().__new__(cls)
vars(res).update(vars(orig))
return res
def __init__(self, *args, **kwargs):
self._headers = self._repair_headers()
# suppress spurious error from mypy
def __iter__(self):
return super().__iter__()
def __getitem__(self, item):
"""
Warn users that a ``KeyError`` can be expected when a
mising key is supplied. Ref python/importlib_metadata#371.
"""
res = super().__getitem__(item)
if res is None:
_warn()
return res
def _repair_headers(self):
def redent(value):
"Correct for RFC822 indentation"
if not value or "\n" not in value:
return value
return textwrap.dedent(" " * 8 + value)
headers = [(key, redent(value)) for key, value in vars(self)["_headers"]]
if self._payload:
headers.append(("Description", self.get_payload()))
return headers
@property
def json(self):
"""
Convert PackageMetadata to a JSON-compatible format
per PEP 0566.
"""
def transform(key):
value = self.get_all(key) if key in self.multiple_use_keys else self[key]
if key == "Keywords":
value = re.split(r"\s+", value)
tk = key.lower().replace("-", "_")
return tk, value
return dict(map(transform, map(FoldedCase, self)))
@@ -0,0 +1,30 @@
import collections
# from jaraco.collections 3.3
class FreezableDefaultDict(collections.defaultdict):
"""
Often it is desirable to prevent the mutation of
a default dict after its initial construction, such
as to prevent mutation during iteration.
>>> dd = FreezableDefaultDict(list)
>>> dd[0].append('1')
>>> dd.freeze()
>>> dd[1]
[]
>>> len(dd)
1
"""
def __missing__(self, key):
return getattr(self, "_frozen", super().__missing__)(key)
def freeze(self):
self._frozen = lambda key: self.default_factory()
class Pair(collections.namedtuple("Pair", "name value")):
@classmethod
def parse(cls, text):
return cls(*map(str.strip, text.split("=", 1)))
@@ -0,0 +1,81 @@
import os
import platform
import sys
from typing import Union
__all__ = ["install", "NullFinder", "Protocol"]
try:
from typing import Protocol
except ImportError: # pragma: no cover
# Python 3.7 compatibility
from typing_extensions import Protocol # type: ignore
def install(cls):
"""
Class decorator for installation on sys.meta_path.
Adds the backport DistributionFinder to sys.meta_path and
attempts to disable the finder functionality of the stdlib
DistributionFinder.
"""
sys.meta_path.append(cls())
disable_stdlib_finder()
return cls
def disable_stdlib_finder():
"""
Give the backport primacy for discovering path-based distributions
by monkey-patching the stdlib O_O.
See #91 for more background for rationale on this sketchy
behavior.
"""
def matches(finder):
return getattr(
finder, "__module__", None
) == "_frozen_importlib_external" and hasattr(finder, "find_distributions")
for finder in filter(matches, sys.meta_path): # pragma: nocover
del finder.find_distributions
class NullFinder:
"""
A "Finder" (aka "MetaClassFinder") that never finds any modules,
but may find distributions.
"""
@staticmethod
def find_spec(*args, **kwargs):
return None
# In Python 2, the import system requires finders
# to have a find_module() method, but this usage
# is deprecated in Python 3 in favor of find_spec().
# For the purposes of this finder (i.e. being present
# on sys.meta_path but having no other import
# system functionality), the two methods are identical.
find_module = find_spec
def pypy_partial(val):
"""
Adjust for variable stacklevel on partial under PyPy.
Workaround for #327.
"""
is_pypy = platform.python_implementation() == "PyPy"
return val + is_pypy
if sys.version_info >= (3, 9):
StrPath = Union[str, os.PathLike[str]]
else:
# PathLike is only subscriptable at runtime in 3.9+
StrPath = Union[str, "os.PathLike[str]"] # pragma: no cover
@@ -0,0 +1,104 @@
import functools
import types
# from jaraco.functools 3.3
def method_cache(method, cache_wrapper=None):
"""
Wrap lru_cache to support storing the cache data in the object instances.
Abstracts the common paradigm where the method explicitly saves an
underscore-prefixed protected property on first call and returns that
subsequently.
>>> class MyClass:
... calls = 0
...
... @method_cache
... def method(self, value):
... self.calls += 1
... return value
>>> a = MyClass()
>>> a.method(3)
3
>>> for x in range(75):
... res = a.method(x)
>>> a.calls
75
Note that the apparent behavior will be exactly like that of lru_cache
except that the cache is stored on each instance, so values in one
instance will not flush values from another, and when an instance is
deleted, so are the cached values for that instance.
>>> b = MyClass()
>>> for x in range(35):
... res = b.method(x)
>>> b.calls
35
>>> a.method(0)
0
>>> a.calls
75
Note that if method had been decorated with ``functools.lru_cache()``,
a.calls would have been 76 (due to the cached value of 0 having been
flushed by the 'b' instance).
Clear the cache with ``.cache_clear()``
>>> a.method.cache_clear()
Same for a method that hasn't yet been called.
>>> c = MyClass()
>>> c.method.cache_clear()
Another cache wrapper may be supplied:
>>> cache = functools.lru_cache(maxsize=2)
>>> MyClass.method2 = method_cache(lambda self: 3, cache_wrapper=cache)
>>> a = MyClass()
>>> a.method2()
3
Caution - do not subsequently wrap the method with another decorator, such
as ``@property``, which changes the semantics of the function.
See also
http://code.activestate.com/recipes/577452-a-memoize-decorator-for-instance-methods/
for another implementation and additional justification.
"""
cache_wrapper = cache_wrapper or functools.lru_cache()
def wrapper(self, *args, **kwargs):
# it's the first call, replace the method with a cached, bound method
bound_method = types.MethodType(method, self)
cached_method = cache_wrapper(bound_method)
setattr(self, method.__name__, cached_method)
return cached_method(*args, **kwargs)
# Support cache clear even before cache has been created.
wrapper.cache_clear = lambda: None
return wrapper
# From jaraco.functools 3.3
def pass_none(func):
"""
Wrap func so it's not called if its first param is None
>>> print_text = pass_none(print)
>>> print_text('text')
text
>>> print_text(None)
"""
@functools.wraps(func)
def wrapper(param, *args, **kwargs):
if param is not None:
return func(param, *args, **kwargs)
return wrapper
@@ -0,0 +1,73 @@
from itertools import filterfalse
def unique_everseen(iterable, key=None):
"List unique elements, preserving order. Remember all elements ever seen."
# unique_everseen('AAAABBBCCDAABBB') --> A B C D
# unique_everseen('ABBCcAD', str.lower) --> A B C D
seen = set()
seen_add = seen.add
if key is None:
for element in filterfalse(seen.__contains__, iterable):
seen_add(element)
yield element
else:
for element in iterable:
k = key(element)
if k not in seen:
seen_add(k)
yield element
# copied from more_itertools 8.8
def always_iterable(obj, base_type=(str, bytes)):
"""If *obj* is iterable, return an iterator over its items::
>>> obj = (1, 2, 3)
>>> list(always_iterable(obj))
[1, 2, 3]
If *obj* is not iterable, return a one-item iterable containing *obj*::
>>> obj = 1
>>> list(always_iterable(obj))
[1]
If *obj* is ``None``, return an empty iterable:
>>> obj = None
>>> list(always_iterable(None))
[]
By default, binary and text strings are not considered iterable::
>>> obj = 'foo'
>>> list(always_iterable(obj))
['foo']
If *base_type* is set, objects for which ``isinstance(obj, base_type)``
returns ``True`` won't be considered iterable.
>>> obj = {'a': 1}
>>> list(always_iterable(obj)) # Iterate over the dict's keys
['a']
>>> list(always_iterable(obj, base_type=dict)) # Treat dicts as a unit
[{'a': 1}]
Set *base_type* to ``None`` to avoid any special handling and treat objects
Python considers iterable as iterable:
>>> obj = 'foo'
>>> list(always_iterable(obj, base_type=None))
['f', 'o', 'o']
"""
if obj is None:
return iter(())
if (base_type is not None) and isinstance(obj, base_type):
return iter((obj,))
try:
return iter(obj)
except TypeError:
return iter((obj,))
@@ -0,0 +1,63 @@
from typing import Any, Dict, Iterator, List, Optional, TypeVar, Union, overload
from ._compat import Protocol
_T = TypeVar("_T")
class PackageMetadata(Protocol):
def __len__(self) -> int:
... # pragma: no cover
def __contains__(self, item: str) -> bool:
... # pragma: no cover
def __getitem__(self, key: str) -> str:
... # pragma: no cover
def __iter__(self) -> Iterator[str]:
... # pragma: no cover
@overload
def get(self, name: str, failobj: None = None) -> Optional[str]:
... # pragma: no cover
@overload
def get(self, name: str, failobj: _T) -> Union[str, _T]:
... # pragma: no cover
# overload per python/importlib_metadata#435
@overload
def get_all(self, name: str, failobj: None = None) -> Optional[List[Any]]:
... # pragma: no cover
@overload
def get_all(self, name: str, failobj: _T) -> Union[List[Any], _T]:
"""
Return all values associated with a possibly multi-valued key.
"""
@property
def json(self) -> Dict[str, Union[str, List[str]]]:
"""
A JSON-compatible form of the metadata.
"""
class SimplePath(Protocol[_T]):
"""
A minimal subset of pathlib.Path required by PathDistribution.
"""
def joinpath(self, other: Union[str, _T]) -> _T:
... # pragma: no cover
def __truediv__(self, other: Union[str, _T]) -> _T:
... # pragma: no cover
@property
def parent(self) -> _T:
... # pragma: no cover
def read_text(self) -> str:
... # pragma: no cover
@@ -0,0 +1,35 @@
"""
Compatibility layer with Python 3.8/3.9
"""
from typing import TYPE_CHECKING, Any, Optional
if TYPE_CHECKING: # pragma: no cover
# Prevent circular imports on runtime.
from . import Distribution, EntryPoint
else:
Distribution = EntryPoint = Any
def normalized_name(dist: Distribution) -> Optional[str]:
"""
Honor name normalization for distributions that don't provide ``_normalized_name``.
"""
try:
return dist._normalized_name
except AttributeError:
from . import Prepared # -> delay to prevent circular imports.
return Prepared.normalize(getattr(dist, "name", None) or dist.metadata["Name"])
def ep_matches(ep: EntryPoint, **params) -> bool:
"""
Workaround for ``EntryPoint`` objects without the ``matches`` method.
"""
try:
return ep.matches(**params)
except AttributeError:
from . import EntryPoint # -> delay to prevent circular imports.
# Reconstruct the EntryPoint object to make sure it is compatible.
return EntryPoint(ep.name, ep.value, ep.group).matches(**params)
@@ -0,0 +1,99 @@
import re
from ._functools import method_cache
# from jaraco.text 3.5
class FoldedCase(str):
"""
A case insensitive string class; behaves just like str
except compares equal when the only variation is case.
>>> s = FoldedCase('hello world')
>>> s == 'Hello World'
True
>>> 'Hello World' == s
True
>>> s != 'Hello World'
False
>>> s.index('O')
4
>>> s.split('O')
['hell', ' w', 'rld']
>>> sorted(map(FoldedCase, ['GAMMA', 'alpha', 'Beta']))
['alpha', 'Beta', 'GAMMA']
Sequence membership is straightforward.
>>> "Hello World" in [s]
True
>>> s in ["Hello World"]
True
You may test for set inclusion, but candidate and elements
must both be folded.
>>> FoldedCase("Hello World") in {s}
True
>>> s in {FoldedCase("Hello World")}
True
String inclusion works as long as the FoldedCase object
is on the right.
>>> "hello" in FoldedCase("Hello World")
True
But not if the FoldedCase object is on the left:
>>> FoldedCase('hello') in 'Hello World'
False
In that case, use in_:
>>> FoldedCase('hello').in_('Hello World')
True
>>> FoldedCase('hello') > FoldedCase('Hello')
False
"""
def __lt__(self, other):
return self.lower() < other.lower()
def __gt__(self, other):
return self.lower() > other.lower()
def __eq__(self, other):
return self.lower() == other.lower()
def __ne__(self, other):
return self.lower() != other.lower()
def __hash__(self):
return hash(self.lower())
def __contains__(self, other):
return super().lower().__contains__(other.lower())
def in_(self, other):
"Does self appear in other?"
return self in FoldedCase(other)
# cache lower since it's likely to be called frequently.
@method_cache
def lower(self):
return super().lower()
def index(self, sub):
return self.lower().index(sub.lower())
def split(self, splitter=" ", maxsplit=0):
pattern = re.compile(re.escape(splitter), re.I)
return pattern.split(self, maxsplit)
+126
View File
@@ -0,0 +1,126 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
The OpenTelemetry metrics API describes the classes used to generate
metrics.
The :class:`.MeterProvider` provides users access to the :class:`.Meter` which in
turn is used to create :class:`.Instrument` objects. The :class:`.Instrument` objects are
used to record measurements.
This module provides abstract (i.e. unimplemented) classes required for
metrics, and a concrete no-op implementation :class:`.NoOpMeter` that allows applications
to use the API package alone without a supporting implementation.
To get a meter, you need to provide the package name from which you are
calling the meter APIs to OpenTelemetry by calling `MeterProvider.get_meter`
with the calling instrumentation name and the version of your package.
The following code shows how to obtain a meter using the global :class:`.MeterProvider`::
from mysql.opentelemetry.metrics import get_meter
meter = get_meter("example-meter")
counter = meter.create_counter("example-counter")
.. versionadded:: 1.10.0
.. versionchanged:: 1.12.0rc
"""
from mysql.opentelemetry.metrics._internal import (
Meter,
MeterProvider,
NoOpMeter,
NoOpMeterProvider,
get_meter,
get_meter_provider,
set_meter_provider,
)
from mysql.opentelemetry.metrics._internal.instrument import (
Asynchronous,
CallbackOptions,
CallbackT,
Counter,
Histogram,
Instrument,
NoOpCounter,
NoOpHistogram,
NoOpObservableCounter,
NoOpObservableGauge,
NoOpObservableUpDownCounter,
NoOpUpDownCounter,
ObservableCounter,
ObservableGauge,
ObservableUpDownCounter,
Synchronous,
UpDownCounter,
)
from mysql.opentelemetry.metrics._internal.observation import Observation
for obj in [
Counter,
Synchronous,
Asynchronous,
CallbackOptions,
get_meter_provider,
get_meter,
Histogram,
Meter,
MeterProvider,
Instrument,
NoOpCounter,
NoOpHistogram,
NoOpMeter,
NoOpMeterProvider,
NoOpObservableCounter,
NoOpObservableGauge,
NoOpObservableUpDownCounter,
NoOpUpDownCounter,
ObservableCounter,
ObservableGauge,
ObservableUpDownCounter,
Observation,
set_meter_provider,
UpDownCounter,
]:
obj.__module__ = __name__
__all__ = [
"CallbackOptions",
"MeterProvider",
"NoOpMeterProvider",
"Meter",
"Counter",
"NoOpCounter",
"UpDownCounter",
"NoOpUpDownCounter",
"Histogram",
"NoOpHistogram",
"ObservableCounter",
"NoOpObservableCounter",
"ObservableUpDownCounter",
"Instrument",
"Synchronous",
"Asynchronous",
"NoOpObservableGauge",
"ObservableGauge",
"NoOpObservableUpDownCounter",
"get_meter",
"get_meter_provider",
"set_meter_provider",
"Observation",
"CallbackT",
"NoOpMeter",
]
@@ -0,0 +1,759 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# pylint: disable=too-many-ancestors
"""
The OpenTelemetry metrics API describes the classes used to generate
metrics.
The :class:`.MeterProvider` provides users access to the :class:`.Meter` which in
turn is used to create :class:`.Instrument` objects. The :class:`.Instrument` objects are
used to record measurements.
This module provides abstract (i.e. unimplemented) classes required for
metrics, and a concrete no-op implementation :class:`.NoOpMeter` that allows applications
to use the API package alone without a supporting implementation.
To get a meter, you need to provide the package name from which you are
calling the meter APIs to OpenTelemetry by calling `MeterProvider.get_meter`
with the calling instrumentation name and the version of your package.
The following code shows how to obtain a meter using the global :class:`.MeterProvider`::
from mysql.opentelemetry.metrics import get_meter
meter = get_meter("example-meter")
counter = meter.create_counter("example-counter")
.. versionadded:: 1.10.0
"""
from abc import ABC, abstractmethod
from logging import getLogger
from os import environ
from threading import Lock
from typing import List, Optional, Sequence, Set, Tuple, Union, cast
from mysql.opentelemetry.environment_variables import OTEL_PYTHON_METER_PROVIDER
from mysql.opentelemetry.metrics._internal.instrument import (
CallbackT,
Counter,
Histogram,
NoOpCounter,
NoOpHistogram,
NoOpObservableCounter,
NoOpObservableGauge,
NoOpObservableUpDownCounter,
NoOpUpDownCounter,
ObservableCounter,
ObservableGauge,
ObservableUpDownCounter,
UpDownCounter,
_ProxyCounter,
_ProxyHistogram,
_ProxyObservableCounter,
_ProxyObservableGauge,
_ProxyObservableUpDownCounter,
_ProxyUpDownCounter,
)
from mysql.opentelemetry.util._once import Once
from mysql.opentelemetry.util._providers import _load_provider
_logger = getLogger(__name__)
_ProxyInstrumentT = Union[
_ProxyCounter,
_ProxyHistogram,
_ProxyObservableCounter,
_ProxyObservableGauge,
_ProxyObservableUpDownCounter,
_ProxyUpDownCounter,
]
class MeterProvider(ABC):
"""
MeterProvider is the entry point of the API. It provides access to `Meter` instances.
"""
@abstractmethod
def get_meter(
self,
name: str,
version: Optional[str] = None,
schema_url: Optional[str] = None,
) -> "Meter":
"""Returns a `Meter` for use by the given instrumentation library.
For any two calls it is undefined whether the same or different
`Meter` instances are returned, even for different library names.
This function may return different `Meter` types (e.g. a no-op meter
vs. a functional meter).
Args:
name: The name of the instrumenting module.
``__name__`` may not be used as this can result in
different meter names if the meters are in different files.
It is better to use a fixed string that can be imported where
needed and used consistently as the name of the meter.
This should *not* be the name of the module that is
instrumented but the name of the module doing the instrumentation.
E.g., instead of ``"requests"``, use
``"mysql.opentelemetry.instrumentation.requests"``.
version: Optional. The version string of the
instrumenting library. Usually this should be the same as
``importlib.metadata.version(instrumenting_library_name)``.
schema_url: Optional. Specifies the Schema URL of the emitted telemetry.
"""
class NoOpMeterProvider(MeterProvider):
"""The default MeterProvider used when no MeterProvider implementation is available."""
def get_meter(
self,
name: str,
version: Optional[str] = None,
schema_url: Optional[str] = None,
) -> "Meter":
"""Returns a NoOpMeter."""
super().get_meter(name, version=version, schema_url=schema_url)
return NoOpMeter(name, version=version, schema_url=schema_url)
class _ProxyMeterProvider(MeterProvider):
def __init__(self) -> None:
self._lock = Lock()
self._meters: List[_ProxyMeter] = []
self._real_meter_provider: Optional[MeterProvider] = None
def get_meter(
self,
name: str,
version: Optional[str] = None,
schema_url: Optional[str] = None,
) -> "Meter":
with self._lock:
if self._real_meter_provider is not None:
return self._real_meter_provider.get_meter(name, version, schema_url)
meter = _ProxyMeter(name, version=version, schema_url=schema_url)
self._meters.append(meter)
return meter
def on_set_meter_provider(self, meter_provider: MeterProvider) -> None:
with self._lock:
self._real_meter_provider = meter_provider
for meter in self._meters:
meter.on_set_meter_provider(meter_provider)
class Meter(ABC):
"""Handles instrument creation.
This class provides methods for creating instruments which are then
used to produce measurements.
"""
def __init__(
self,
name: str,
version: Optional[str] = None,
schema_url: Optional[str] = None,
) -> None:
super().__init__()
self._name = name
self._version = version
self._schema_url = schema_url
self._instrument_ids: Set[str] = set()
self._instrument_ids_lock = Lock()
@property
def name(self) -> str:
"""
The name of the instrumenting module.
"""
return self._name
@property
def version(self) -> Optional[str]:
"""
The version string of the instrumenting library.
"""
return self._version
@property
def schema_url(self) -> Optional[str]:
"""
Specifies the Schema URL of the emitted telemetry
"""
return self._schema_url
def _is_instrument_registered(
self, name: str, type_: type, unit: str, description: str
) -> Tuple[bool, str]:
"""
Check if an instrument with the same name, type, unit and description
has been registered already.
Returns a tuple. The first value is `True` if the instrument has been
registered already, `False` otherwise. The second value is the
instrument id.
"""
instrument_id = ",".join(
[name.strip().lower(), type_.__name__, unit, description]
)
result = False
with self._instrument_ids_lock:
if instrument_id in self._instrument_ids:
result = True
else:
self._instrument_ids.add(instrument_id)
return (result, instrument_id)
@abstractmethod
def create_counter(
self,
name: str,
unit: str = "",
description: str = "",
) -> Counter:
"""Creates a `Counter` instrument
Args:
name: The name of the instrument to be created
unit: The unit for observations this instrument reports. For
example, ``By`` for bytes. UCUM units are recommended.
description: A description for this instrument and what it measures.
"""
@abstractmethod
def create_up_down_counter(
self,
name: str,
unit: str = "",
description: str = "",
) -> UpDownCounter:
"""Creates an `UpDownCounter` instrument
Args:
name: The name of the instrument to be created
unit: The unit for observations this instrument reports. For
example, ``By`` for bytes. UCUM units are recommended.
description: A description for this instrument and what it measures.
"""
@abstractmethod
def create_observable_counter(
self,
name: str,
callbacks: Optional[Sequence[CallbackT]] = None,
unit: str = "",
description: str = "",
) -> ObservableCounter:
"""Creates an `ObservableCounter` instrument
An observable counter observes a monotonically increasing count by calling provided
callbacks which accept a :class:`~mysql.opentelemetry.metrics.CallbackOptions` and return
multiple :class:`~mysql.opentelemetry.metrics.Observation`.
For example, an observable counter could be used to report system CPU
time periodically. Here is a basic implementation::
def cpu_time_callback(options: CallbackOptions) -> Iterable[Observation]:
observations = []
with open("/proc/stat") as procstat:
procstat.readline() # skip the first line
for line in procstat:
if not line.startswith("cpu"): break
cpu, *states = line.split()
observations.append(Observation(int(states[0]) // 100, {"cpu": cpu, "state": "user"}))
observations.append(Observation(int(states[1]) // 100, {"cpu": cpu, "state": "nice"}))
observations.append(Observation(int(states[2]) // 100, {"cpu": cpu, "state": "system"}))
# ... other states
return observations
meter.create_observable_counter(
"system.cpu.time",
callbacks=[cpu_time_callback],
unit="s",
description="CPU time"
)
To reduce memory usage, you can use generator callbacks instead of
building the full list::
def cpu_time_callback(options: CallbackOptions) -> Iterable[Observation]:
with open("/proc/stat") as procstat:
procstat.readline() # skip the first line
for line in procstat:
if not line.startswith("cpu"): break
cpu, *states = line.split()
yield Observation(int(states[0]) // 100, {"cpu": cpu, "state": "user"})
yield Observation(int(states[1]) // 100, {"cpu": cpu, "state": "nice"})
# ... other states
Alternatively, you can pass a sequence of generators directly instead of a sequence of
callbacks, which each should return iterables of :class:`~mysql.opentelemetry.metrics.Observation`::
def cpu_time_callback(states_to_include: set[str]) -> Iterable[Iterable[Observation]]:
# accept options sent in from OpenTelemetry
options = yield
while True:
observations = []
with open("/proc/stat") as procstat:
procstat.readline() # skip the first line
for line in procstat:
if not line.startswith("cpu"): break
cpu, *states = line.split()
if "user" in states_to_include:
observations.append(Observation(int(states[0]) // 100, {"cpu": cpu, "state": "user"}))
if "nice" in states_to_include:
observations.append(Observation(int(states[1]) // 100, {"cpu": cpu, "state": "nice"}))
# ... other states
# yield the observations and receive the options for next iteration
options = yield observations
meter.create_observable_counter(
"system.cpu.time",
callbacks=[cpu_time_callback({"user", "system"})],
unit="s",
description="CPU time"
)
The :class:`~mysql.opentelemetry.metrics.CallbackOptions` contain a timeout which the
callback should respect. For example if the callback does asynchronous work, like
making HTTP requests, it should respect the timeout::
def scrape_http_callback(options: CallbackOptions) -> Iterable[Observation]:
r = requests.get('http://scrapethis.com', timeout=options.timeout_millis / 10**3)
for value in r.json():
yield Observation(value)
Args:
name: The name of the instrument to be created
callbacks: A sequence of callbacks that return an iterable of
:class:`~mysql.opentelemetry.metrics.Observation`. Alternatively, can be a sequence of generators that each
yields iterables of :class:`~mysql.opentelemetry.metrics.Observation`.
unit: The unit for observations this instrument reports. For
example, ``By`` for bytes. UCUM units are recommended.
description: A description for this instrument and what it measures.
"""
@abstractmethod
def create_histogram(
self,
name: str,
unit: str = "",
description: str = "",
) -> Histogram:
"""Creates a :class:`~mysql.opentelemetry.metrics.Histogram` instrument
Args:
name: The name of the instrument to be created
unit: The unit for observations this instrument reports. For
example, ``By`` for bytes. UCUM units are recommended.
description: A description for this instrument and what it measures.
"""
@abstractmethod
def create_observable_gauge(
self,
name: str,
callbacks: Optional[Sequence[CallbackT]] = None,
unit: str = "",
description: str = "",
) -> ObservableGauge:
"""Creates an `ObservableGauge` instrument
Args:
name: The name of the instrument to be created
callbacks: A sequence of callbacks that return an iterable of
:class:`~mysql.opentelemetry.metrics.Observation`. Alternatively, can be a generator that yields iterables
of :class:`~mysql.opentelemetry.metrics.Observation`.
unit: The unit for observations this instrument reports. For
example, ``By`` for bytes. UCUM units are recommended.
description: A description for this instrument and what it measures.
"""
@abstractmethod
def create_observable_up_down_counter(
self,
name: str,
callbacks: Optional[Sequence[CallbackT]] = None,
unit: str = "",
description: str = "",
) -> ObservableUpDownCounter:
"""Creates an `ObservableUpDownCounter` instrument
Args:
name: The name of the instrument to be created
callbacks: A sequence of callbacks that return an iterable of
:class:`~mysql.opentelemetry.metrics.Observation`. Alternatively, can be a generator that yields iterables
of :class:`~mysql.opentelemetry.metrics.Observation`.
unit: The unit for observations this instrument reports. For
example, ``By`` for bytes. UCUM units are recommended.
description: A description for this instrument and what it measures.
"""
class _ProxyMeter(Meter):
def __init__(
self,
name: str,
version: Optional[str] = None,
schema_url: Optional[str] = None,
) -> None:
super().__init__(name, version=version, schema_url=schema_url)
self._lock = Lock()
self._instruments: List[_ProxyInstrumentT] = []
self._real_meter: Optional[Meter] = None
def on_set_meter_provider(self, meter_provider: MeterProvider) -> None:
"""Called when a real meter provider is set on the creating _ProxyMeterProvider
Creates a real backing meter for this instance and notifies all created
instruments so they can create real backing instruments.
"""
real_meter = meter_provider.get_meter(
self._name, self._version, self._schema_url
)
with self._lock:
self._real_meter = real_meter
# notify all proxy instruments of the new meter so they can create
# real instruments to back themselves
for instrument in self._instruments:
instrument.on_meter_set(real_meter)
def create_counter(
self,
name: str,
unit: str = "",
description: str = "",
) -> Counter:
with self._lock:
if self._real_meter:
return self._real_meter.create_counter(name, unit, description)
proxy = _ProxyCounter(name, unit, description)
self._instruments.append(proxy)
return proxy
def create_up_down_counter(
self,
name: str,
unit: str = "",
description: str = "",
) -> UpDownCounter:
with self._lock:
if self._real_meter:
return self._real_meter.create_up_down_counter(name, unit, description)
proxy = _ProxyUpDownCounter(name, unit, description)
self._instruments.append(proxy)
return proxy
def create_observable_counter(
self,
name: str,
callbacks: Optional[Sequence[CallbackT]] = None,
unit: str = "",
description: str = "",
) -> ObservableCounter:
with self._lock:
if self._real_meter:
return self._real_meter.create_observable_counter(
name, callbacks, unit, description
)
proxy = _ProxyObservableCounter(
name, callbacks, unit=unit, description=description
)
self._instruments.append(proxy)
return proxy
def create_histogram(
self,
name: str,
unit: str = "",
description: str = "",
) -> Histogram:
with self._lock:
if self._real_meter:
return self._real_meter.create_histogram(name, unit, description)
proxy = _ProxyHistogram(name, unit, description)
self._instruments.append(proxy)
return proxy
def create_observable_gauge(
self,
name: str,
callbacks: Optional[Sequence[CallbackT]] = None,
unit: str = "",
description: str = "",
) -> ObservableGauge:
with self._lock:
if self._real_meter:
return self._real_meter.create_observable_gauge(
name, callbacks, unit, description
)
proxy = _ProxyObservableGauge(
name, callbacks, unit=unit, description=description
)
self._instruments.append(proxy)
return proxy
def create_observable_up_down_counter(
self,
name: str,
callbacks: Optional[Sequence[CallbackT]] = None,
unit: str = "",
description: str = "",
) -> ObservableUpDownCounter:
with self._lock:
if self._real_meter:
return self._real_meter.create_observable_up_down_counter(
name,
callbacks,
unit,
description,
)
proxy = _ProxyObservableUpDownCounter(
name, callbacks, unit=unit, description=description
)
self._instruments.append(proxy)
return proxy
class NoOpMeter(Meter):
"""The default Meter used when no Meter implementation is available.
All operations are no-op.
"""
def create_counter(
self,
name: str,
unit: str = "",
description: str = "",
) -> Counter:
"""Returns a no-op Counter."""
super().create_counter(name, unit=unit, description=description)
if self._is_instrument_registered(name, NoOpCounter, unit, description)[0]:
_logger.warning(
"An instrument with name %s, type %s, unit %s and "
"description %s has been created already.",
name,
Counter.__name__,
unit,
description,
)
return NoOpCounter(name, unit=unit, description=description)
def create_up_down_counter(
self,
name: str,
unit: str = "",
description: str = "",
) -> UpDownCounter:
"""Returns a no-op UpDownCounter."""
super().create_up_down_counter(name, unit=unit, description=description)
if self._is_instrument_registered(name, NoOpUpDownCounter, unit, description)[
0
]:
_logger.warning(
"An instrument with name %s, type %s, unit %s and "
"description %s has been created already.",
name,
UpDownCounter.__name__,
unit,
description,
)
return NoOpUpDownCounter(name, unit=unit, description=description)
def create_observable_counter(
self,
name: str,
callbacks: Optional[Sequence[CallbackT]] = None,
unit: str = "",
description: str = "",
) -> ObservableCounter:
"""Returns a no-op ObservableCounter."""
super().create_observable_counter(
name, callbacks, unit=unit, description=description
)
if self._is_instrument_registered(
name, NoOpObservableCounter, unit, description
)[0]:
_logger.warning(
"An instrument with name %s, type %s, unit %s and "
"description %s has been created already.",
name,
ObservableCounter.__name__,
unit,
description,
)
return NoOpObservableCounter(
name,
callbacks,
unit=unit,
description=description,
)
def create_histogram(
self,
name: str,
unit: str = "",
description: str = "",
) -> Histogram:
"""Returns a no-op Histogram."""
super().create_histogram(name, unit=unit, description=description)
if self._is_instrument_registered(name, NoOpHistogram, unit, description)[0]:
_logger.warning(
"An instrument with name %s, type %s, unit %s and "
"description %s has been created already.",
name,
Histogram.__name__,
unit,
description,
)
return NoOpHistogram(name, unit=unit, description=description)
def create_observable_gauge(
self,
name: str,
callbacks: Optional[Sequence[CallbackT]] = None,
unit: str = "",
description: str = "",
) -> ObservableGauge:
"""Returns a no-op ObservableGauge."""
super().create_observable_gauge(
name, callbacks, unit=unit, description=description
)
if self._is_instrument_registered(name, NoOpObservableGauge, unit, description)[
0
]:
_logger.warning(
"An instrument with name %s, type %s, unit %s and "
"description %s has been created already.",
name,
ObservableGauge.__name__,
unit,
description,
)
return NoOpObservableGauge(
name,
callbacks,
unit=unit,
description=description,
)
def create_observable_up_down_counter(
self,
name: str,
callbacks: Optional[Sequence[CallbackT]] = None,
unit: str = "",
description: str = "",
) -> ObservableUpDownCounter:
"""Returns a no-op ObservableUpDownCounter."""
super().create_observable_up_down_counter(
name, callbacks, unit=unit, description=description
)
if self._is_instrument_registered(
name, NoOpObservableUpDownCounter, unit, description
)[0]:
_logger.warning(
"An instrument with name %s, type %s, unit %s and "
"description %s has been created already.",
name,
ObservableUpDownCounter.__name__,
unit,
description,
)
return NoOpObservableUpDownCounter(
name,
callbacks,
unit=unit,
description=description,
)
_METER_PROVIDER_SET_ONCE = Once()
_METER_PROVIDER: Optional[MeterProvider] = None
_PROXY_METER_PROVIDER = _ProxyMeterProvider()
def get_meter(
name: str,
version: str = "",
meter_provider: Optional[MeterProvider] = None,
) -> "Meter":
"""Returns a `Meter` for use by the given instrumentation library.
This function is a convenience wrapper for
`mysql.opentelemetry.metrics.MeterProvider.get_meter`.
If meter_provider is omitted the current configured one is used.
"""
if meter_provider is None:
meter_provider = get_meter_provider()
return meter_provider.get_meter(name, version)
def _set_meter_provider(meter_provider: MeterProvider, log: bool) -> None:
def set_mp() -> None:
global _METER_PROVIDER # pylint: disable=global-statement
_METER_PROVIDER = meter_provider
# gives all proxies real instruments off the newly set meter provider
_PROXY_METER_PROVIDER.on_set_meter_provider(meter_provider)
did_set = _METER_PROVIDER_SET_ONCE.do_once(set_mp)
if log and not did_set:
_logger.warning("Overriding of current MeterProvider is not allowed")
def set_meter_provider(meter_provider: MeterProvider) -> None:
"""Sets the current global :class:`~.MeterProvider` object.
This can only be done once, a warning will be logged if any further attempt
is made.
"""
_set_meter_provider(meter_provider, log=True)
def get_meter_provider() -> MeterProvider:
"""Gets the current global :class:`~.MeterProvider` object."""
if _METER_PROVIDER is None:
if OTEL_PYTHON_METER_PROVIDER not in environ.keys():
return _PROXY_METER_PROVIDER
meter_provider: MeterProvider = _load_provider( # type: ignore
OTEL_PYTHON_METER_PROVIDER, "meter_provider"
)
_set_meter_provider(meter_provider, log=False)
# _METER_PROVIDER will have been set by one thread
return cast("MeterProvider", _METER_PROVIDER)
@@ -0,0 +1,389 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# pylint: disable=too-many-ancestors
from abc import ABC, abstractmethod
from dataclasses import dataclass
from logging import getLogger
from re import compile as re_compile
from typing import (
Callable,
Dict,
Generator,
Generic,
Iterable,
Optional,
Sequence,
TypeVar,
Union,
)
# pylint: disable=unused-import; needed for typing and sphinx
from mysql.opentelemetry import metrics
from mysql.opentelemetry.metrics._internal.observation import Observation
from mysql.opentelemetry.util.types import Attributes
_logger = getLogger(__name__)
_name_regex = re_compile(r"[a-zA-Z][-_.a-zA-Z0-9]{0,62}")
_unit_regex = re_compile(r"[\x00-\x7F]{0,63}")
@dataclass(frozen=True)
class CallbackOptions:
"""Options for the callback
Args:
timeout_millis: Timeout for the callback's execution. If the callback does asynchronous
work (e.g. HTTP requests), it should respect this timeout.
"""
timeout_millis: float = 10_000
InstrumentT = TypeVar("InstrumentT", bound="Instrument")
CallbackT = Union[
Callable[[CallbackOptions], Iterable[Observation]],
Generator[Iterable[Observation], CallbackOptions, None],
]
class Instrument(ABC):
"""Abstract class that serves as base for all instruments."""
@abstractmethod
def __init__(
self,
name: str,
unit: str = "",
description: str = "",
) -> None:
pass
@staticmethod
def _check_name_unit_description(
name: str, unit: str, description: str
) -> Dict[str, Optional[str]]:
"""
Checks the following instrument name, unit and description for
compliance with the spec.
Returns a dict with keys "name", "unit" and "description", the
corresponding values will be the checked strings or `None` if the value
is invalid. If valid, the checked strings should be used instead of the
original values.
"""
result: Dict[str, Optional[str]] = {}
if _name_regex.fullmatch(name) is not None:
result["name"] = name
else:
result["name"] = None
if unit is None:
unit = ""
if _unit_regex.fullmatch(unit) is not None:
result["unit"] = unit
else:
result["unit"] = None
if description is None:
result["description"] = ""
else:
result["description"] = description
return result
class _ProxyInstrument(ABC, Generic[InstrumentT]):
def __init__(
self,
name: str,
unit: str = "",
description: str = "",
) -> None:
self._name = name
self._unit = unit
self._description = description
self._real_instrument: Optional[InstrumentT] = None
def on_meter_set(self, meter: "metrics.Meter") -> None:
"""Called when a real meter is set on the creating _ProxyMeter"""
# We don't need any locking on proxy instruments because it's OK if some
# measurements get dropped while a real backing instrument is being
# created.
self._real_instrument = self._create_real_instrument(meter)
@abstractmethod
def _create_real_instrument(self, meter: "metrics.Meter") -> InstrumentT:
"""Create an instance of the real instrument. Implement this."""
class _ProxyAsynchronousInstrument(_ProxyInstrument[InstrumentT]):
def __init__(
self,
name: str,
callbacks: Optional[Sequence[CallbackT]] = None,
unit: str = "",
description: str = "",
) -> None:
super().__init__(name, unit, description)
self._callbacks = callbacks
class Synchronous(Instrument):
"""Base class for all synchronous instruments"""
class Asynchronous(Instrument):
"""Base class for all asynchronous instruments"""
@abstractmethod
def __init__(
self,
name: str,
callbacks: Optional[Sequence[CallbackT]] = None,
unit: str = "",
description: str = "",
) -> None:
super().__init__(name, unit=unit, description=description)
class Counter(Synchronous):
"""A Counter is a synchronous `Instrument` which supports non-negative increments."""
@abstractmethod
def add(
self,
amount: Union[int, float],
attributes: Optional[Attributes] = None,
) -> None:
pass
class NoOpCounter(Counter):
"""No-op implementation of `Counter`."""
def __init__(
self,
name: str,
unit: str = "",
description: str = "",
) -> None:
super().__init__(name, unit=unit, description=description)
def add(
self,
amount: Union[int, float],
attributes: Optional[Attributes] = None,
) -> None:
return super().add(amount, attributes=attributes)
class _ProxyCounter(_ProxyInstrument[Counter], Counter):
def add(
self,
amount: Union[int, float],
attributes: Optional[Attributes] = None,
) -> None:
if self._real_instrument:
self._real_instrument.add(amount, attributes)
def _create_real_instrument(self, meter: "metrics.Meter") -> Counter:
return meter.create_counter(self._name, self._unit, self._description)
class UpDownCounter(Synchronous):
"""An UpDownCounter is a synchronous `Instrument` which supports increments and decrements."""
@abstractmethod
def add(
self,
amount: Union[int, float],
attributes: Optional[Attributes] = None,
) -> None:
pass
class NoOpUpDownCounter(UpDownCounter):
"""No-op implementation of `UpDownCounter`."""
def __init__(
self,
name: str,
unit: str = "",
description: str = "",
) -> None:
super().__init__(name, unit=unit, description=description)
def add(
self,
amount: Union[int, float],
attributes: Optional[Attributes] = None,
) -> None:
return super().add(amount, attributes=attributes)
class _ProxyUpDownCounter(_ProxyInstrument[UpDownCounter], UpDownCounter):
def add(
self,
amount: Union[int, float],
attributes: Optional[Attributes] = None,
) -> None:
if self._real_instrument:
self._real_instrument.add(amount, attributes)
def _create_real_instrument(self, meter: "metrics.Meter") -> UpDownCounter:
return meter.create_up_down_counter(self._name, self._unit, self._description)
class ObservableCounter(Asynchronous):
"""An ObservableCounter is an asynchronous `Instrument` which reports monotonically
increasing value(s) when the instrument is being observed.
"""
class NoOpObservableCounter(ObservableCounter):
"""No-op implementation of `ObservableCounter`."""
def __init__(
self,
name: str,
callbacks: Optional[Sequence[CallbackT]] = None,
unit: str = "",
description: str = "",
) -> None:
super().__init__(name, callbacks, unit=unit, description=description)
class _ProxyObservableCounter(
_ProxyAsynchronousInstrument[ObservableCounter], ObservableCounter
):
def _create_real_instrument(self, meter: "metrics.Meter") -> ObservableCounter:
return meter.create_observable_counter(
self._name, self._callbacks, self._unit, self._description
)
class ObservableUpDownCounter(Asynchronous):
"""An ObservableUpDownCounter is an asynchronous `Instrument` which reports additive value(s) (e.g.
the process heap size - it makes sense to report the heap size from multiple processes and sum them
up, so we get the total heap usage) when the instrument is being observed.
"""
class NoOpObservableUpDownCounter(ObservableUpDownCounter):
"""No-op implementation of `ObservableUpDownCounter`."""
def __init__(
self,
name: str,
callbacks: Optional[Sequence[CallbackT]] = None,
unit: str = "",
description: str = "",
) -> None:
super().__init__(name, callbacks, unit=unit, description=description)
class _ProxyObservableUpDownCounter(
_ProxyAsynchronousInstrument[ObservableUpDownCounter],
ObservableUpDownCounter,
):
def _create_real_instrument(
self, meter: "metrics.Meter"
) -> ObservableUpDownCounter:
return meter.create_observable_up_down_counter(
self._name, self._callbacks, self._unit, self._description
)
class Histogram(Synchronous):
"""Histogram is a synchronous `Instrument` which can be used to report arbitrary values
that are likely to be statistically meaningful. It is intended for statistics such as
histograms, summaries, and percentile.
"""
@abstractmethod
def record(
self,
amount: Union[int, float],
attributes: Optional[Attributes] = None,
) -> None:
pass
class NoOpHistogram(Histogram):
"""No-op implementation of `Histogram`."""
def __init__(
self,
name: str,
unit: str = "",
description: str = "",
) -> None:
super().__init__(name, unit=unit, description=description)
def record(
self,
amount: Union[int, float],
attributes: Optional[Attributes] = None,
) -> None:
return super().record(amount, attributes=attributes)
class _ProxyHistogram(_ProxyInstrument[Histogram], Histogram):
def record(
self,
amount: Union[int, float],
attributes: Optional[Attributes] = None,
) -> None:
if self._real_instrument:
self._real_instrument.record(amount, attributes)
def _create_real_instrument(self, meter: "metrics.Meter") -> Histogram:
return meter.create_histogram(self._name, self._unit, self._description)
class ObservableGauge(Asynchronous):
"""Asynchronous Gauge is an asynchronous `Instrument` which reports non-additive value(s) (e.g.
the room temperature - it makes no sense to report the temperature value from multiple rooms
and sum them up) when the instrument is being observed.
"""
class NoOpObservableGauge(ObservableGauge):
"""No-op implementation of `ObservableGauge`."""
def __init__(
self,
name: str,
callbacks: Optional[Sequence[CallbackT]] = None,
unit: str = "",
description: str = "",
) -> None:
super().__init__(name, callbacks, unit=unit, description=description)
class _ProxyObservableGauge(
_ProxyAsynchronousInstrument[ObservableGauge],
ObservableGauge,
):
def _create_real_instrument(self, meter: "metrics.Meter") -> ObservableGauge:
return meter.create_observable_gauge(
self._name, self._callbacks, self._unit, self._description
)
@@ -0,0 +1,50 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Union
from mysql.opentelemetry.util.types import Attributes
class Observation:
"""A measurement observed in an asynchronous instrument
Return/yield instances of this class from asynchronous instrument callbacks.
Args:
value: The float or int measured value
attributes: The measurement's attributes
"""
def __init__(self, value: Union[int, float], attributes: Attributes = None) -> None:
self._value = value
self._attributes = attributes
@property
def value(self) -> Union[float, int]:
return self._value
@property
def attributes(self) -> Attributes:
return self._attributes
def __eq__(self, other: object) -> bool:
return (
isinstance(other, Observation)
and self.value == other.value
and self.attributes == other.attributes
)
def __repr__(self) -> str:
return f"Observation(value={self.value}, attributes={self.attributes})"
+163
View File
@@ -0,0 +1,163 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
API for propagation of context.
The propagators for the
``mysql.opentelemetry.propagators.composite.CompositePropagator`` can be defined
via configuration in the ``OTEL_PROPAGATORS`` environment variable. This
variable should be set to a comma-separated string of names of values for the
``opentelemetry_propagator`` entry point. For example, setting
``OTEL_PROPAGATORS`` to ``tracecontext,baggage`` (which is the default value)
would instantiate
``mysql.opentelemetry.propagators.composite.CompositePropagator`` with 2
propagators, one of type
``mysql.opentelemetry.trace.propagation.tracecontext.TraceContextTextMapPropagator``
and other of type ``mysql.opentelemetry.baggage.propagation.W3CBaggagePropagator``.
Notice that these propagator classes are defined as
``opentelemetry_propagator`` entry points in the ``pyproject.toml`` file of
``opentelemetry``.
Example::
import flask
import requests
from opentelemetry import propagate
PROPAGATOR = propagate.get_global_textmap()
def get_header_from_flask_request(request, key):
return request.headers.get_all(key)
def set_header_into_requests_request(request: requests.Request,
key: str, value: str):
request.headers[key] = value
def example_route():
context = PROPAGATOR.extract(
get_header_from_flask_request,
flask.request
)
request_to_downstream = requests.Request(
"GET", "http://httpbin.org/get"
)
PROPAGATOR.inject(
set_header_into_requests_request,
request_to_downstream,
context=context
)
session = requests.Session()
session.send(request_to_downstream.prepare())
.. _Propagation API Specification:
https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/api-propagators.md
"""
from logging import getLogger
from os import environ
from typing import Optional
from mysql.opentelemetry.context.context import Context
from mysql.opentelemetry.environment_variables import OTEL_PROPAGATORS
from mysql.opentelemetry.propagators import composite, textmap
from mysql.opentelemetry.util._importlib_metadata import entry_points
logger = getLogger(__name__)
def extract(
carrier: textmap.CarrierT,
context: Optional[Context] = None,
getter: textmap.Getter[textmap.CarrierT] = textmap.default_getter,
) -> Context:
"""Uses the configured propagator to extract a Context from the carrier.
Args:
getter: an object which contains a get function that can retrieve zero
or more values from the carrier and a keys function that can get all the keys
from carrier.
carrier: and object which contains values that are
used to construct a Context. This object
must be paired with an appropriate getter
which understands how to extract a value from it.
context: an optional Context to use. Defaults to root
context if not set.
"""
return get_global_textmap().extract(carrier, context, getter=getter)
def inject(
carrier: textmap.CarrierT,
context: Optional[Context] = None,
setter: textmap.Setter[textmap.CarrierT] = textmap.default_setter,
) -> None:
"""Uses the configured propagator to inject a Context into the carrier.
Args:
carrier: An object that contains a representation of HTTP
headers. Should be paired with setter, which
should know how to set header values on the carrier.
context: An optional Context to use. Defaults to current
context if not set.
setter: An optional `Setter` object that can set values
on the carrier.
"""
get_global_textmap().inject(carrier, context=context, setter=setter)
propagators = []
# Single use variable here to hack black and make lint pass
environ_propagators = environ.get(
OTEL_PROPAGATORS,
"tracecontext,baggage",
)
for propagator in environ_propagators.split(","):
propagator = propagator.strip()
try:
propagators.append( # type: ignore
next( # type: ignore
iter( # type: ignore
entry_points( # type: ignore
group="opentelemetry_propagator",
name=propagator,
)
)
).load()()
)
except Exception: # pylint: disable=broad-except
logger.exception("Failed to load configured propagator: %s", propagator)
raise
_HTTP_TEXT_FORMAT = composite.CompositePropagator(propagators) # type: ignore
def get_global_textmap() -> textmap.TextMapPropagator:
return _HTTP_TEXT_FORMAT
def set_global_textmap(
http_text_format: textmap.TextMapPropagator,
) -> None:
global _HTTP_TEXT_FORMAT # pylint:disable=global-statement
_HTTP_TEXT_FORMAT = http_text_format # type: ignore
@@ -0,0 +1,88 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import typing
from deprecated import deprecated
from mysql.opentelemetry.context.context import Context
from mysql.opentelemetry.propagators import textmap
logger = logging.getLogger(__name__)
class CompositePropagator(textmap.TextMapPropagator):
"""CompositePropagator provides a mechanism for combining multiple
propagators into a single one.
Args:
propagators: the list of propagators to use
"""
def __init__(self, propagators: typing.Sequence[textmap.TextMapPropagator]) -> None:
self._propagators = propagators
def extract(
self,
carrier: textmap.CarrierT,
context: typing.Optional[Context] = None,
getter: textmap.Getter[textmap.CarrierT] = textmap.default_getter,
) -> Context:
"""Run each of the configured propagators with the given context and carrier.
Propagators are run in the order they are configured, if multiple
propagators write the same context key, the propagator later in the list
will override previous propagators.
See `mysql.opentelemetry.propagators.textmap.TextMapPropagator.extract`
"""
for propagator in self._propagators:
context = propagator.extract(carrier, context, getter=getter)
return context # type: ignore
def inject(
self,
carrier: textmap.CarrierT,
context: typing.Optional[Context] = None,
setter: textmap.Setter[textmap.CarrierT] = textmap.default_setter,
) -> None:
"""Run each of the configured propagators with the given context and carrier.
Propagators are run in the order they are configured, if multiple
propagators write the same carrier key, the propagator later in the list
will override previous propagators.
See `mysql.opentelemetry.propagators.textmap.TextMapPropagator.inject`
"""
for propagator in self._propagators:
propagator.inject(carrier, context, setter=setter)
@property
def fields(self) -> typing.Set[str]:
"""Returns a set with the fields set in `inject`.
See
`mysql.opentelemetry.propagators.textmap.TextMapPropagator.fields`
"""
composite_fields = set()
for propagator in self._propagators:
for field in propagator.fields:
composite_fields.add(field)
return composite_fields
@deprecated(version="1.2.0", reason="You should use CompositePropagator") # type: ignore
class CompositeHTTPPropagator(CompositePropagator):
"""CompositeHTTPPropagator provides a mechanism for combining multiple
propagators into a single one.
"""
+192
View File
@@ -0,0 +1,192 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import abc
import typing
from mysql.opentelemetry.context.context import Context
CarrierT = typing.TypeVar("CarrierT")
CarrierValT = typing.Union[typing.List[str], str]
class Getter(abc.ABC, typing.Generic[CarrierT]):
"""This class implements a Getter that enables extracting propagated
fields from a carrier.
"""
@abc.abstractmethod
def get(self, carrier: CarrierT, key: str) -> typing.Optional[typing.List[str]]:
"""Function that can retrieve zero
or more values from the carrier. In the case that
the value does not exist, returns None.
Args:
carrier: An object which contains values that are used to
construct a Context.
key: key of a field in carrier.
Returns: first value of the propagation key or None if the key doesn't
exist.
"""
@abc.abstractmethod
def keys(self, carrier: CarrierT) -> typing.List[str]:
"""Function that can retrieve all the keys in a carrier object.
Args:
carrier: An object which contains values that are
used to construct a Context.
Returns:
list of keys from the carrier.
"""
class Setter(abc.ABC, typing.Generic[CarrierT]):
"""This class implements a Setter that enables injecting propagated
fields into a carrier.
"""
@abc.abstractmethod
def set(self, carrier: CarrierT, key: str, value: str) -> None:
"""Function that can set a value into a carrier""
Args:
carrier: An object which contains values that are used to
construct a Context.
key: key of a field in carrier.
value: value for a field in carrier.
"""
class DefaultGetter(Getter[typing.Mapping[str, CarrierValT]]):
def get(
self, carrier: typing.Mapping[str, CarrierValT], key: str
) -> typing.Optional[typing.List[str]]:
"""Getter implementation to retrieve a value from a dictionary.
Args:
carrier: dictionary in which to get value
key: the key used to get the value
Returns:
A list with a single string with the value if it exists, else None.
"""
val = carrier.get(key, None)
if val is None:
return None
if isinstance(val, typing.Iterable) and not isinstance(val, str):
return list(val)
return [val]
def keys(self, carrier: typing.Mapping[str, CarrierValT]) -> typing.List[str]:
"""Keys implementation that returns all keys from a dictionary."""
return list(carrier.keys())
default_getter: Getter[CarrierT] = DefaultGetter() # type: ignore
class DefaultSetter(Setter[typing.MutableMapping[str, CarrierValT]]):
def set(
self,
carrier: typing.MutableMapping[str, CarrierValT],
key: str,
value: CarrierValT,
) -> None:
"""Setter implementation to set a value into a dictionary.
Args:
carrier: dictionary in which to set value
key: the key used to set the value
value: the value to set
"""
carrier[key] = value
default_setter: Setter[CarrierT] = DefaultSetter() # type: ignore
class TextMapPropagator(abc.ABC):
"""This class provides an interface that enables extracting and injecting
context into headers of HTTP requests. HTTP frameworks and clients
can integrate with TextMapPropagator by providing the object containing the
headers, and a getter and setter function for the extraction and
injection of values, respectively.
"""
@abc.abstractmethod
def extract(
self,
carrier: CarrierT,
context: typing.Optional[Context] = None,
getter: Getter[CarrierT] = default_getter,
) -> Context:
"""Create a Context from values in the carrier.
The extract function should retrieve values from the carrier
object using getter, and use values to populate a
Context value and return it.
Args:
getter: a function that can retrieve zero
or more values from the carrier. In the case that
the value does not exist, return an empty list.
carrier: and object which contains values that are
used to construct a Context. This object
must be paired with an appropriate getter
which understands how to extract a value from it.
context: an optional Context to use. Defaults to root
context if not set.
Returns:
A Context with configuration found in the carrier.
"""
@abc.abstractmethod
def inject(
self,
carrier: CarrierT,
context: typing.Optional[Context] = None,
setter: Setter[CarrierT] = default_setter,
) -> None:
"""Inject values from a Context into a carrier.
inject enables the propagation of values into HTTP clients or
other objects which perform an HTTP request. Implementations
should use the `Setter` 's set method to set values on the
carrier.
Args:
carrier: An object that a place to define HTTP headers.
Should be paired with setter, which should
know how to set header values on the carrier.
context: an optional Context to use. Defaults to current
context if not set.
setter: An optional `Setter` object that can set values
on the carrier.
"""
@property
@abc.abstractmethod
def fields(self) -> typing.Set[str]:
"""
Gets the fields set in the carrier by the `inject` method.
If the carrier is reused, its fields that correspond with the ones
present in this attribute should be deleted before calling `inject`.
Returns:
A set with the fields set in `inject`.
"""
View File
View File
@@ -0,0 +1,410 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""
OpenTelemetry SDK Configurator for Easy Instrumentation with Distros
"""
import logging
import os
from abc import ABC, abstractmethod
from os import environ
from typing import Callable, Dict, List, Optional, Sequence, Tuple, Type
from mysql.opentelemetry._logs import set_logger_provider
from mysql.opentelemetry.environment_variables import (
OTEL_LOGS_EXPORTER,
OTEL_METRICS_EXPORTER,
OTEL_PYTHON_ID_GENERATOR,
OTEL_TRACES_EXPORTER,
)
from mysql.opentelemetry.metrics import set_meter_provider
from mysql.opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from mysql.opentelemetry.sdk._logs.export import BatchLogRecordProcessor, LogExporter
from mysql.opentelemetry.sdk.environment_variables import (
_OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED,
OTEL_EXPORTER_OTLP_LOGS_PROTOCOL,
OTEL_EXPORTER_OTLP_METRICS_PROTOCOL,
OTEL_EXPORTER_OTLP_PROTOCOL,
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL,
OTEL_TRACES_SAMPLER,
OTEL_TRACES_SAMPLER_ARG,
)
from mysql.opentelemetry.sdk.metrics import MeterProvider
from mysql.opentelemetry.sdk.metrics.export import (
MetricExporter,
PeriodicExportingMetricReader,
)
from mysql.opentelemetry.sdk.resources import Resource
from mysql.opentelemetry.sdk.trace import TracerProvider
from mysql.opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter
from mysql.opentelemetry.sdk.trace.id_generator import IdGenerator
from mysql.opentelemetry.sdk.trace.sampling import Sampler
from mysql.opentelemetry.semconv.resource import ResourceAttributes
from mysql.opentelemetry.trace import set_tracer_provider
from mysql.opentelemetry.util._importlib_metadata import entry_points
from typing_extensions import Literal
_EXPORTER_OTLP = "otlp"
_EXPORTER_OTLP_PROTO_GRPC = "otlp_proto_grpc"
_EXPORTER_OTLP_PROTO_HTTP = "otlp_proto_http"
_EXPORTER_BY_OTLP_PROTOCOL = {
"grpc": _EXPORTER_OTLP_PROTO_GRPC,
"http/protobuf": _EXPORTER_OTLP_PROTO_HTTP,
}
_EXPORTER_ENV_BY_SIGNAL_TYPE = {
"traces": OTEL_TRACES_EXPORTER,
"metrics": OTEL_METRICS_EXPORTER,
"logs": OTEL_LOGS_EXPORTER,
}
_PROTOCOL_ENV_BY_SIGNAL_TYPE = {
"traces": OTEL_EXPORTER_OTLP_TRACES_PROTOCOL,
"metrics": OTEL_EXPORTER_OTLP_METRICS_PROTOCOL,
"logs": OTEL_EXPORTER_OTLP_LOGS_PROTOCOL,
}
_RANDOM_ID_GENERATOR = "random"
_DEFAULT_ID_GENERATOR = _RANDOM_ID_GENERATOR
_OTEL_SAMPLER_ENTRY_POINT_GROUP = "opentelemetry_traces_sampler"
_logger = logging.getLogger(__name__)
def _import_config_components(
selected_components: List[str], entry_point_name: str
) -> Sequence[Tuple[str, object]]:
component_implementations = []
for selected_component in selected_components:
try:
component_implementations.append(
(
selected_component,
next(
iter(
entry_points(
group=entry_point_name, name=selected_component
)
)
).load(),
)
)
except KeyError:
raise RuntimeError(f"Requested entry point '{entry_point_name}' not found")
except StopIteration:
raise RuntimeError(
f"Requested component '{selected_component}' not found in "
f"entry point '{entry_point_name}'"
)
return component_implementations
def _get_sampler() -> Optional[str]:
return environ.get(OTEL_TRACES_SAMPLER, None)
def _get_id_generator() -> str:
return environ.get(OTEL_PYTHON_ID_GENERATOR, _DEFAULT_ID_GENERATOR)
def _get_exporter_entry_point(
exporter_name: str, signal_type: Literal["traces", "metrics", "logs"]
):
if exporter_name not in (
_EXPORTER_OTLP,
_EXPORTER_OTLP_PROTO_GRPC,
_EXPORTER_OTLP_PROTO_HTTP,
):
return exporter_name
# Checking env vars for OTLP protocol (grpc/http).
otlp_protocol = environ.get(
_PROTOCOL_ENV_BY_SIGNAL_TYPE[signal_type]
) or environ.get(OTEL_EXPORTER_OTLP_PROTOCOL)
if not otlp_protocol:
if exporter_name == _EXPORTER_OTLP:
return _EXPORTER_OTLP_PROTO_GRPC
return exporter_name
otlp_protocol = otlp_protocol.strip()
if exporter_name == _EXPORTER_OTLP:
if otlp_protocol not in _EXPORTER_BY_OTLP_PROTOCOL:
# Invalid value was set by the env var
raise RuntimeError(
f"Unsupported OTLP protocol '{otlp_protocol}' is configured"
)
return _EXPORTER_BY_OTLP_PROTOCOL[otlp_protocol]
# grpc/http already specified by exporter_name, only add a warning in case
# of a conflict.
exporter_name_by_env = _EXPORTER_BY_OTLP_PROTOCOL.get(otlp_protocol)
if exporter_name_by_env and exporter_name != exporter_name_by_env:
_logger.warning(
"Conflicting values for %s OTLP exporter protocol, using '%s'",
signal_type,
exporter_name,
)
return exporter_name
def _get_exporter_names(
signal_type: Literal["traces", "metrics", "logs"]
) -> Sequence[str]:
names = environ.get(_EXPORTER_ENV_BY_SIGNAL_TYPE.get(signal_type, ""))
if not names or names.lower().strip() == "none":
return []
return [
_get_exporter_entry_point(_exporter.strip(), signal_type)
for _exporter in names.split(",")
]
def _init_tracing(
exporters: Dict[str, Type[SpanExporter]],
id_generator: IdGenerator = None,
sampler: Sampler = None,
resource: Resource = None,
):
provider = TracerProvider(
id_generator=id_generator,
sampler=sampler,
resource=resource,
)
set_tracer_provider(provider)
for _, exporter_class in exporters.items():
exporter_args = {}
provider.add_span_processor(BatchSpanProcessor(exporter_class(**exporter_args)))
def _init_metrics(
exporters: Dict[str, Type[MetricExporter]],
resource: Resource = None,
):
metric_readers = []
for _, exporter_class in exporters.items():
exporter_args = {}
metric_readers.append(
PeriodicExportingMetricReader(exporter_class(**exporter_args))
)
provider = MeterProvider(resource=resource, metric_readers=metric_readers)
set_meter_provider(provider)
def _init_logging(
exporters: Dict[str, Type[LogExporter]],
resource: Resource = None,
):
provider = LoggerProvider(resource=resource)
set_logger_provider(provider)
for _, exporter_class in exporters.items():
exporter_args = {}
provider.add_log_record_processor(
BatchLogRecordProcessor(exporter_class(**exporter_args))
)
handler = LoggingHandler(level=logging.NOTSET, logger_provider=provider)
logging.getLogger().addHandler(handler)
def _import_exporters(
trace_exporter_names: Sequence[str],
metric_exporter_names: Sequence[str],
log_exporter_names: Sequence[str],
) -> Tuple[
Dict[str, Type[SpanExporter]],
Dict[str, Type[MetricExporter]],
Dict[str, Type[LogExporter]],
]:
trace_exporters = {}
metric_exporters = {}
log_exporters = {}
for (
exporter_name,
exporter_impl,
) in _import_config_components(
trace_exporter_names, "opentelemetry_traces_exporter"
):
if issubclass(exporter_impl, SpanExporter):
trace_exporters[exporter_name] = exporter_impl
else:
raise RuntimeError(f"{exporter_name} is not a trace exporter")
for (
exporter_name,
exporter_impl,
) in _import_config_components(
metric_exporter_names, "opentelemetry_metrics_exporter"
):
if issubclass(exporter_impl, MetricExporter):
metric_exporters[exporter_name] = exporter_impl
else:
raise RuntimeError(f"{exporter_name} is not a metric exporter")
for (
exporter_name,
exporter_impl,
) in _import_config_components(log_exporter_names, "opentelemetry_logs_exporter"):
if issubclass(exporter_impl, LogExporter):
log_exporters[exporter_name] = exporter_impl
else:
raise RuntimeError(f"{exporter_name} is not a log exporter")
return trace_exporters, metric_exporters, log_exporters
def _import_sampler_factory(sampler_name: str) -> Callable[[str], Sampler]:
_, sampler_impl = _import_config_components(
[sampler_name.strip()], _OTEL_SAMPLER_ENTRY_POINT_GROUP
)[0]
return sampler_impl
def _import_sampler(sampler_name: str) -> Optional[Sampler]:
if not sampler_name:
return None
try:
sampler_factory = _import_sampler_factory(sampler_name)
arg = None
if sampler_name in ("traceidratio", "parentbased_traceidratio"):
try:
rate = float(os.getenv(OTEL_TRACES_SAMPLER_ARG))
except (ValueError, TypeError):
_logger.warning(
"Could not convert TRACES_SAMPLER_ARG to float. Using default value 1.0."
)
rate = 1.0
arg = rate
else:
arg = os.getenv(OTEL_TRACES_SAMPLER_ARG)
sampler = sampler_factory(arg)
if not isinstance(sampler, Sampler):
message = f"Sampler factory, {sampler_factory}, produced output, {sampler}, which is not a Sampler."
_logger.warning(message)
raise ValueError(message)
return sampler
except Exception as exc: # pylint: disable=broad-except
_logger.warning(
"Using default sampler. Failed to initialize sampler, %s: %s",
sampler_name,
exc,
)
return None
def _import_id_generator(id_generator_name: str) -> IdGenerator:
id_generator_name, id_generator_impl = _import_config_components(
[id_generator_name.strip()], "opentelemetry_id_generator"
)[0]
if issubclass(id_generator_impl, IdGenerator):
return id_generator_impl()
raise RuntimeError(f"{id_generator_name} is not an IdGenerator")
def _initialize_components(auto_instrumentation_version):
trace_exporters, metric_exporters, log_exporters = _import_exporters(
_get_exporter_names("traces"),
_get_exporter_names("metrics"),
_get_exporter_names("logs"),
)
sampler_name = _get_sampler()
sampler = _import_sampler(sampler_name)
id_generator_name = _get_id_generator()
id_generator = _import_id_generator(id_generator_name)
# if env var OTEL_RESOURCE_ATTRIBUTES is given, it will read the service_name
# from the env variable else defaults to "unknown_service"
auto_resource = {}
# populate version if using auto-instrumentation
if auto_instrumentation_version:
auto_resource[
ResourceAttributes.TELEMETRY_AUTO_VERSION
] = auto_instrumentation_version
resource = Resource.create(auto_resource)
_init_tracing(
exporters=trace_exporters,
id_generator=id_generator,
sampler=sampler,
resource=resource,
)
_init_metrics(metric_exporters, resource)
logging_enabled = os.getenv(
_OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED, "false"
)
if logging_enabled.strip().lower() == "true":
_init_logging(log_exporters, resource)
class _BaseConfigurator(ABC):
"""An ABC for configurators
Configurators are used to configure
SDKs (i.e. TracerProvider, MeterProvider, Processors...)
to reduce the amount of manual configuration required.
"""
_instance = None
_is_instrumented = False
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = object.__new__(cls, *args, **kwargs)
return cls._instance
@abstractmethod
def _configure(self, **kwargs):
"""Configure the SDK"""
def configure(self, **kwargs):
"""Configure the SDK"""
self._configure(**kwargs)
class _OTelSDKConfigurator(_BaseConfigurator):
"""A basic Configurator by OTel Python for initializing OTel SDK components
Initializes several crucial OTel SDK components (i.e. TracerProvider,
MeterProvider, Processors...) according to a default implementation. Other
Configurators can subclass and slightly alter this initialization.
NOTE: This class should not be instantiated nor should it become an entry
point on the `opentelemetry-sdk` package. Instead, distros should subclass
this Configurator and enchance it as needed.
"""
def _configure(self, **kwargs):
_initialize_components(kwargs.get("auto_instrumentation_version"))
+32
View File
@@ -0,0 +1,32 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from mysql.opentelemetry.sdk._logs._internal import (
LogData,
Logger,
LoggerProvider,
LoggingHandler,
LogRecord,
LogRecordProcessor,
)
__all__ = [
"LogData",
"Logger",
"LoggerProvider",
"LoggingHandler",
"LogRecord",
"LogRecordProcessor",
]
@@ -0,0 +1,470 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import abc
import atexit
import concurrent.futures
import json
import logging
import threading
import traceback
from time import time_ns
from typing import Any, Callable, Optional, Tuple, Union
from mysql.opentelemetry._logs import (
Logger as APILogger,
LoggerProvider as APILoggerProvider,
LogRecord as APILogRecord,
SeverityNumber,
get_logger,
get_logger_provider,
std_to_otel,
)
from mysql.opentelemetry.sdk.resources import Resource
from mysql.opentelemetry.sdk.util import ns_to_iso_str
from mysql.opentelemetry.sdk.util.instrumentation import InstrumentationScope
from mysql.opentelemetry.semconv.trace import SpanAttributes
from mysql.opentelemetry.trace import format_span_id, format_trace_id, get_current_span
from mysql.opentelemetry.trace.span import TraceFlags
from mysql.opentelemetry.util.types import Attributes
_logger = logging.getLogger(__name__)
class LogRecord(APILogRecord):
"""A LogRecord instance represents an event being logged.
LogRecord instances are created and emitted via `Logger`
every time something is logged. They contain all the information
pertinent to the event being logged.
"""
def __init__(
self,
timestamp: Optional[int] = None,
observed_timestamp: Optional[int] = None,
trace_id: Optional[int] = None,
span_id: Optional[int] = None,
trace_flags: Optional[TraceFlags] = None,
severity_text: Optional[str] = None,
severity_number: Optional[SeverityNumber] = None,
body: Optional[Any] = None,
resource: Optional[Resource] = None,
attributes: Optional[Attributes] = None,
):
super().__init__(
**{
"timestamp": timestamp,
"observed_timestamp": observed_timestamp,
"trace_id": trace_id,
"span_id": span_id,
"trace_flags": trace_flags,
"severity_text": severity_text,
"severity_number": severity_number,
"body": body,
"attributes": attributes,
}
)
self.resource = resource
def __eq__(self, other: object) -> bool:
if not isinstance(other, LogRecord):
return NotImplemented
return self.__dict__ == other.__dict__
def to_json(self, indent=4) -> str:
return json.dumps(
{
"body": self.body,
"severity_number": repr(self.severity_number),
"severity_text": self.severity_text,
"attributes": self.attributes,
"timestamp": ns_to_iso_str(self.timestamp),
"trace_id": f"0x{format_trace_id(self.trace_id)}"
if self.trace_id is not None
else "",
"span_id": f"0x{format_span_id(self.span_id)}"
if self.span_id is not None
else "",
"trace_flags": self.trace_flags,
"resource": repr(self.resource.attributes) if self.resource else "",
},
indent=indent,
)
class LogData:
"""Readable LogRecord data plus associated InstrumentationLibrary."""
def __init__(
self,
log_record: LogRecord,
instrumentation_scope: InstrumentationScope,
):
self.log_record = log_record
self.instrumentation_scope = instrumentation_scope
class LogRecordProcessor(abc.ABC):
"""Interface to hook the log record emitting action.
Log processors can be registered directly using
:func:`LoggerProvider.add_log_record_processor` and they are invoked
in the same order as they were registered.
"""
@abc.abstractmethod
def emit(self, log_data: LogData):
"""Emits the `LogData`"""
@abc.abstractmethod
def shutdown(self):
"""Called when a :class:`mysql.opentelemetry.sdk._logs.Logger` is shutdown"""
@abc.abstractmethod
def force_flush(self, timeout_millis: int = 30000):
"""Export all the received logs to the configured Exporter that have not yet
been exported.
Args:
timeout_millis: The maximum amount of time to wait for logs to be
exported.
Returns:
False if the timeout is exceeded, True otherwise.
"""
# Temporary fix until https://github.com/PyCQA/pylint/issues/4098 is resolved
# pylint:disable=no-member
class SynchronousMultiLogRecordProcessor(LogRecordProcessor):
"""Implementation of class:`LogRecordProcessor` that forwards all received
events to a list of log processors sequentially.
The underlying log processors are called in sequential order as they were
added.
"""
def __init__(self):
# use a tuple to avoid race conditions when adding a new log and
# iterating through it on "emit".
self._log_record_processors = () # type: Tuple[LogRecordProcessor, ...]
self._lock = threading.Lock()
def add_log_record_processor(
self, log_record_processor: LogRecordProcessor
) -> None:
"""Adds a Logprocessor to the list of log processors handled by this instance"""
with self._lock:
self._log_record_processors += (log_record_processor,)
def emit(self, log_data: LogData) -> None:
for lp in self._log_record_processors:
lp.emit(log_data)
def shutdown(self) -> None:
"""Shutdown the log processors one by one"""
for lp in self._log_record_processors:
lp.shutdown()
def force_flush(self, timeout_millis: int = 30000) -> bool:
"""Force flush the log processors one by one
Args:
timeout_millis: The maximum amount of time to wait for logs to be
exported. If the first n log processors exceeded the timeout
then remaining log processors will not be flushed.
Returns:
True if all the log processors flushes the logs within timeout,
False otherwise.
"""
deadline_ns = time_ns() + timeout_millis * 1000000
for lp in self._log_record_processors:
current_ts = time_ns()
if current_ts >= deadline_ns:
return False
if not lp.force_flush((deadline_ns - current_ts) // 1000000):
return False
return True
class ConcurrentMultiLogRecordProcessor(LogRecordProcessor):
"""Implementation of :class:`LogRecordProcessor` that forwards all received
events to a list of log processors in parallel.
Calls to the underlying log processors are forwarded in parallel by
submitting them to a thread pool executor and waiting until each log
processor finished its work.
Args:
max_workers: The number of threads managed by the thread pool executor
and thus defining how many log processors can work in parallel.
"""
def __init__(self, max_workers: int = 2):
# use a tuple to avoid race conditions when adding a new log and
# iterating through it on "emit".
self._log_record_processors = () # type: Tuple[LogRecordProcessor, ...]
self._lock = threading.Lock()
self._executor = concurrent.futures.ThreadPoolExecutor(max_workers=max_workers)
def add_log_record_processor(self, log_record_processor: LogRecordProcessor):
with self._lock:
self._log_record_processors += (log_record_processor,)
def _submit_and_wait(
self,
func: Callable[[LogRecordProcessor], Callable[..., None]],
*args: Any,
**kwargs: Any,
):
futures = []
for lp in self._log_record_processors:
future = self._executor.submit(func(lp), *args, **kwargs)
futures.append(future)
for future in futures:
future.result()
def emit(self, log_data: LogData):
self._submit_and_wait(lambda lp: lp.emit, log_data)
def shutdown(self):
self._submit_and_wait(lambda lp: lp.shutdown)
def force_flush(self, timeout_millis: int = 30000) -> bool:
"""Force flush the log processors in parallel.
Args:
timeout_millis: The maximum amount of time to wait for logs to be
exported.
Returns:
True if all the log processors flushes the logs within timeout,
False otherwise.
"""
futures = []
for lp in self._log_record_processors:
future = self._executor.submit(lp.force_flush, timeout_millis)
futures.append(future)
done_futures, not_done_futures = concurrent.futures.wait(
futures, timeout_millis / 1e3
)
if not_done_futures:
return False
for future in done_futures:
if not future.result():
return False
return True
# skip natural LogRecord attributes
# http://docs.python.org/library/logging.html#logrecord-attributes
_RESERVED_ATTRS = frozenset(
(
"asctime",
"args",
"created",
"exc_info",
"exc_text",
"filename",
"funcName",
"message",
"levelname",
"levelno",
"lineno",
"module",
"msecs",
"msg",
"name",
"pathname",
"process",
"processName",
"relativeCreated",
"stack_info",
"thread",
"threadName",
)
)
class LoggingHandler(logging.Handler):
"""A handler class which writes logging records, in OTLP format, to
a network destination or file. Supports signals from the `logging` module.
https://docs.python.org/3/library/logging.html
"""
def __init__(
self,
level=logging.NOTSET,
logger_provider=None,
) -> None:
super().__init__(level=level)
self._logger_provider = logger_provider or get_logger_provider()
self._logger = get_logger(__name__, logger_provider=self._logger_provider)
@staticmethod
def _get_attributes(record: logging.LogRecord) -> Attributes:
attributes = {k: v for k, v in vars(record).items() if k not in _RESERVED_ATTRS}
if record.exc_info:
exc_type = ""
message = ""
stack_trace = ""
exctype, value, tb = record.exc_info
if exctype is not None:
exc_type = exctype.__name__
if value is not None and value.args:
message = value.args[0]
if tb is not None:
# https://github.com/open-telemetry/opentelemetry-specification/blob/9fa7c656b26647b27e485a6af7e38dc716eba98a/specification/trace/semantic_conventions/exceptions.md#stacktrace-representation
stack_trace = "".join(traceback.format_exception(*record.exc_info))
attributes[SpanAttributes.EXCEPTION_TYPE] = exc_type
attributes[SpanAttributes.EXCEPTION_MESSAGE] = message
attributes[SpanAttributes.EXCEPTION_STACKTRACE] = stack_trace
return attributes
def _translate(self, record: logging.LogRecord) -> LogRecord:
timestamp = int(record.created * 1e9)
span_context = get_current_span().get_span_context()
attributes = self._get_attributes(record)
severity_number = std_to_otel(record.levelno)
return LogRecord(
timestamp=timestamp,
trace_id=span_context.trace_id,
span_id=span_context.span_id,
trace_flags=span_context.trace_flags,
severity_text=record.levelname,
severity_number=severity_number,
body=record.getMessage(),
resource=self._logger.resource,
attributes=attributes,
)
def emit(self, record: logging.LogRecord) -> None:
"""
Emit a record.
The record is translated to OTel format, and then sent across the pipeline.
"""
self._logger.emit(self._translate(record))
def flush(self) -> None:
"""
Flushes the logging output.
"""
self._logger_provider.force_flush()
class Logger(APILogger):
def __init__(
self,
resource: Resource,
multi_log_record_processor: Union[
SynchronousMultiLogRecordProcessor,
ConcurrentMultiLogRecordProcessor,
],
instrumentation_scope: InstrumentationScope,
):
super().__init__(
instrumentation_scope.name,
instrumentation_scope.version,
instrumentation_scope.schema_url,
)
self._resource = resource
self._multi_log_record_processor = multi_log_record_processor
self._instrumentation_scope = instrumentation_scope
@property
def resource(self):
return self._resource
def emit(self, record: LogRecord):
"""Emits the :class:`LogData` by associating :class:`LogRecord`
and instrumentation info.
"""
log_data = LogData(record, self._instrumentation_scope)
self._multi_log_record_processor.emit(log_data)
class LoggerProvider(APILoggerProvider):
def __init__(
self,
resource: Resource = Resource.create(),
shutdown_on_exit: bool = True,
multi_log_record_processor: Union[
SynchronousMultiLogRecordProcessor,
ConcurrentMultiLogRecordProcessor,
] = None,
):
self._resource = resource
self._multi_log_record_processor = (
multi_log_record_processor or SynchronousMultiLogRecordProcessor()
)
self._at_exit_handler = None
if shutdown_on_exit:
self._at_exit_handler = atexit.register(self.shutdown)
@property
def resource(self):
return self._resource
def get_logger(
self,
name: str,
version: Optional[str] = None,
schema_url: Optional[str] = None,
) -> Logger:
return Logger(
self._resource,
self._multi_log_record_processor,
InstrumentationScope(
name,
version,
schema_url,
),
)
def add_log_record_processor(self, log_record_processor: LogRecordProcessor):
"""Registers a new :class:`LogRecordProcessor` for this `LoggerProvider` instance.
The log processors are invoked in the same order they are registered.
"""
self._multi_log_record_processor.add_log_record_processor(log_record_processor)
def shutdown(self):
"""Shuts down the log processors."""
self._multi_log_record_processor.shutdown()
if self._at_exit_handler is not None:
atexit.unregister(self._at_exit_handler)
self._at_exit_handler = None
def force_flush(self, timeout_millis: int = 30000) -> bool:
"""Force flush the log processors.
Args:
timeout_millis: The maximum amount of time to wait for logs to be
exported.
Returns:
True if all the log processors flushes the logs within timeout,
False otherwise.
"""
return self._multi_log_record_processor.force_flush(timeout_millis)
@@ -0,0 +1,455 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import abc
import collections
import enum
import logging
import os
import sys
import threading
from os import environ, linesep
from time import time_ns
from typing import IO, Callable, Deque, List, Optional, Sequence
from mysql.opentelemetry.context import (
_SUPPRESS_INSTRUMENTATION_KEY,
attach,
detach,
set_value,
)
from mysql.opentelemetry.sdk._logs import LogData, LogRecord, LogRecordProcessor
from mysql.opentelemetry.sdk.environment_variables import (
OTEL_BLRP_EXPORT_TIMEOUT,
OTEL_BLRP_MAX_EXPORT_BATCH_SIZE,
OTEL_BLRP_MAX_QUEUE_SIZE,
OTEL_BLRP_SCHEDULE_DELAY,
)
from mysql.opentelemetry.util._once import Once
_DEFAULT_SCHEDULE_DELAY_MILLIS = 5000
_DEFAULT_MAX_EXPORT_BATCH_SIZE = 512
_DEFAULT_EXPORT_TIMEOUT_MILLIS = 30000
_DEFAULT_MAX_QUEUE_SIZE = 2048
_ENV_VAR_INT_VALUE_ERROR_MESSAGE = (
"Unable to parse value for %s as integer. Defaulting to %s."
)
_logger = logging.getLogger(__name__)
class LogExportResult(enum.Enum):
SUCCESS = 0
FAILURE = 1
class LogExporter(abc.ABC):
"""Interface for exporting logs.
Interface to be implemented by services that want to export logs received
in their own format.
To export data this MUST be registered to the :class`mysql.opentelemetry.sdk._logs.Logger` using a
log processor.
"""
@abc.abstractmethod
def export(self, batch: Sequence[LogData]):
"""Exports a batch of logs.
Args:
batch: The list of `LogData` objects to be exported
Returns:
The result of the export
"""
@abc.abstractmethod
def shutdown(self):
"""Shuts down the exporter.
Called when the SDK is shut down.
"""
class ConsoleLogExporter(LogExporter):
"""Implementation of :class:`LogExporter` that prints log records to the
console.
This class can be used for diagnostic purposes. It prints the exported
log records to the console STDOUT.
"""
def __init__(
self,
out: IO = sys.stdout,
formatter: Callable[[LogRecord], str] = lambda record: record.to_json()
+ linesep,
):
self.out = out
self.formatter = formatter
def export(self, batch: Sequence[LogData]):
for data in batch:
self.out.write(self.formatter(data.log_record))
self.out.flush()
return LogExportResult.SUCCESS
def shutdown(self):
pass
class SimpleLogRecordProcessor(LogRecordProcessor):
"""This is an implementation of LogRecordProcessor which passes
received logs in the export-friendly LogData representation to the
configured LogExporter, as soon as they are emitted.
"""
def __init__(self, exporter: LogExporter):
self._exporter = exporter
self._shutdown = False
def emit(self, log_data: LogData):
if self._shutdown:
_logger.warning("Processor is already shutdown, ignoring call")
return
token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True))
try:
self._exporter.export((log_data,))
except Exception: # pylint: disable=broad-except
_logger.exception("Exception while exporting logs.")
detach(token)
def shutdown(self):
self._shutdown = True
self._exporter.shutdown()
def force_flush(
self, timeout_millis: int = 30000
) -> bool: # pylint: disable=no-self-use
return True
class _FlushRequest:
__slots__ = ["event", "num_log_records"]
def __init__(self):
self.event = threading.Event()
self.num_log_records = 0
_BSP_RESET_ONCE = Once()
class BatchLogRecordProcessor(LogRecordProcessor):
"""This is an implementation of LogRecordProcessor which creates batches of
received logs in the export-friendly LogData representation and
send to the configured LogExporter, as soon as they are emitted.
`BatchLogRecordProcessor` is configurable with the following environment
variables which correspond to constructor parameters:
- :envvar:`OTEL_BLRP_SCHEDULE_DELAY`
- :envvar:`OTEL_BLRP_MAX_QUEUE_SIZE`
- :envvar:`OTEL_BLRP_MAX_EXPORT_BATCH_SIZE`
- :envvar:`OTEL_BLRP_EXPORT_TIMEOUT`
"""
def __init__(
self,
exporter: LogExporter,
schedule_delay_millis: float = None,
max_export_batch_size: int = None,
export_timeout_millis: float = None,
max_queue_size: int = None,
):
if max_queue_size is None:
max_queue_size = BatchLogRecordProcessor._default_max_queue_size()
if schedule_delay_millis is None:
schedule_delay_millis = (
BatchLogRecordProcessor._default_schedule_delay_millis()
)
if max_export_batch_size is None:
max_export_batch_size = (
BatchLogRecordProcessor._default_max_export_batch_size()
)
if export_timeout_millis is None:
export_timeout_millis = (
BatchLogRecordProcessor._default_export_timeout_millis()
)
BatchLogRecordProcessor._validate_arguments(
max_queue_size, schedule_delay_millis, max_export_batch_size
)
self._exporter = exporter
self._max_queue_size = max_queue_size
self._schedule_delay_millis = schedule_delay_millis
self._max_export_batch_size = max_export_batch_size
self._export_timeout_millis = export_timeout_millis
self._queue = collections.deque([], max_queue_size) # type: Deque[LogData]
self._worker_thread = threading.Thread(
name="OtelBatchLogRecordProcessor",
target=self.worker,
daemon=True,
)
self._condition = threading.Condition(threading.Lock())
self._shutdown = False
self._flush_request = None # type: Optional[_FlushRequest]
self._log_records = [
None
] * self._max_export_batch_size # type: List[Optional[LogData]]
self._worker_thread.start()
# Only available in *nix since py37.
if hasattr(os, "register_at_fork"):
os.register_at_fork(
after_in_child=self._at_fork_reinit
) # pylint: disable=protected-access
self._pid = os.getpid()
def _at_fork_reinit(self):
self._condition = threading.Condition(threading.Lock())
self._queue.clear()
self._worker_thread = threading.Thread(
name="OtelBatchLogRecordProcessor",
target=self.worker,
daemon=True,
)
self._worker_thread.start()
self._pid = os.getpid()
def worker(self):
timeout = self._schedule_delay_millis / 1e3
flush_request = None # type: Optional[_FlushRequest]
while not self._shutdown:
with self._condition:
if self._shutdown:
# shutdown may have been called, avoid further processing
break
flush_request = self._get_and_unset_flush_request()
if (
len(self._queue) < self._max_export_batch_size
and flush_request is None
):
self._condition.wait(timeout)
flush_request = self._get_and_unset_flush_request()
if not self._queue:
timeout = self._schedule_delay_millis / 1e3
self._notify_flush_request_finished(flush_request)
flush_request = None
continue
if self._shutdown:
break
start_ns = time_ns()
self._export(flush_request)
end_ns = time_ns()
# subtract the duration of this export call to the next timeout
timeout = self._schedule_delay_millis / 1e3 - ((end_ns - start_ns) / 1e9)
self._notify_flush_request_finished(flush_request)
flush_request = None
# there might have been a new flush request while export was running
# and before the done flag switched to true
with self._condition:
shutdown_flush_request = self._get_and_unset_flush_request()
# flush the remaining logs
self._drain_queue()
self._notify_flush_request_finished(flush_request)
self._notify_flush_request_finished(shutdown_flush_request)
def _export(self, flush_request: Optional[_FlushRequest] = None):
"""Exports logs considering the given flush_request.
If flush_request is not None then logs are exported in batches
until the number of exported logs reached or exceeded the num of logs in
flush_request, otherwise exports at max max_export_batch_size logs.
"""
if flush_request is None:
self._export_batch()
return
num_log_records = flush_request.num_log_records
while self._queue:
exported = self._export_batch()
num_log_records -= exported
if num_log_records <= 0:
break
def _export_batch(self) -> int:
"""Exports at most max_export_batch_size logs and returns the number of
exported logs.
"""
idx = 0
while idx < self._max_export_batch_size and self._queue:
record = self._queue.pop()
self._log_records[idx] = record
idx += 1
token = attach(set_value("suppress_instrumentation", True))
try:
self._exporter.export(self._log_records[:idx]) # type: ignore
except Exception: # pylint: disable=broad-except
_logger.exception("Exception while exporting logs.")
detach(token)
for index in range(idx):
self._log_records[index] = None
return idx
def _drain_queue(self):
"""Export all elements until queue is empty.
Can only be called from the worker thread context because it invokes
`export` that is not thread safe.
"""
while self._queue:
self._export_batch()
def _get_and_unset_flush_request(self) -> Optional[_FlushRequest]:
flush_request = self._flush_request
self._flush_request = None
if flush_request is not None:
flush_request.num_log_records = len(self._queue)
return flush_request
@staticmethod
def _notify_flush_request_finished(
flush_request: Optional[_FlushRequest] = None,
):
if flush_request is not None:
flush_request.event.set()
def _get_or_create_flush_request(self) -> _FlushRequest:
if self._flush_request is None:
self._flush_request = _FlushRequest()
return self._flush_request
def emit(self, log_data: LogData) -> None:
"""Adds the `LogData` to queue and notifies the waiting threads
when size of queue reaches max_export_batch_size.
"""
if self._shutdown:
return
if self._pid != os.getpid():
_BSP_RESET_ONCE.do_once(self._at_fork_reinit)
self._queue.appendleft(log_data)
if len(self._queue) >= self._max_export_batch_size:
with self._condition:
self._condition.notify()
def shutdown(self):
self._shutdown = True
with self._condition:
self._condition.notify_all()
self._worker_thread.join()
self._exporter.shutdown()
def force_flush(self, timeout_millis: Optional[int] = None) -> bool:
if timeout_millis is None:
timeout_millis = self._export_timeout_millis
if self._shutdown:
return True
with self._condition:
flush_request = self._get_or_create_flush_request()
self._condition.notify_all()
ret = flush_request.event.wait(timeout_millis / 1e3)
if not ret:
_logger.warning("Timeout was exceeded in force_flush().")
return ret
@staticmethod
def _default_max_queue_size():
try:
return int(environ.get(OTEL_BLRP_MAX_QUEUE_SIZE, _DEFAULT_MAX_QUEUE_SIZE))
except ValueError:
_logger.exception(
_ENV_VAR_INT_VALUE_ERROR_MESSAGE,
OTEL_BLRP_MAX_QUEUE_SIZE,
_DEFAULT_MAX_QUEUE_SIZE,
)
return _DEFAULT_MAX_QUEUE_SIZE
@staticmethod
def _default_schedule_delay_millis():
try:
return int(
environ.get(OTEL_BLRP_SCHEDULE_DELAY, _DEFAULT_SCHEDULE_DELAY_MILLIS)
)
except ValueError:
_logger.exception(
_ENV_VAR_INT_VALUE_ERROR_MESSAGE,
OTEL_BLRP_SCHEDULE_DELAY,
_DEFAULT_SCHEDULE_DELAY_MILLIS,
)
return _DEFAULT_SCHEDULE_DELAY_MILLIS
@staticmethod
def _default_max_export_batch_size():
try:
return int(
environ.get(
OTEL_BLRP_MAX_EXPORT_BATCH_SIZE,
_DEFAULT_MAX_EXPORT_BATCH_SIZE,
)
)
except ValueError:
_logger.exception(
_ENV_VAR_INT_VALUE_ERROR_MESSAGE,
OTEL_BLRP_MAX_EXPORT_BATCH_SIZE,
_DEFAULT_MAX_EXPORT_BATCH_SIZE,
)
return _DEFAULT_MAX_EXPORT_BATCH_SIZE
@staticmethod
def _default_export_timeout_millis():
try:
return int(
environ.get(OTEL_BLRP_EXPORT_TIMEOUT, _DEFAULT_EXPORT_TIMEOUT_MILLIS)
)
except ValueError:
_logger.exception(
_ENV_VAR_INT_VALUE_ERROR_MESSAGE,
OTEL_BLRP_EXPORT_TIMEOUT,
_DEFAULT_EXPORT_TIMEOUT_MILLIS,
)
return _DEFAULT_EXPORT_TIMEOUT_MILLIS
@staticmethod
def _validate_arguments(
max_queue_size, schedule_delay_millis, max_export_batch_size
):
if max_queue_size <= 0:
raise ValueError("max_queue_size must be a positive integer.")
if schedule_delay_millis <= 0:
raise ValueError("schedule_delay_millis must be positive.")
if max_export_batch_size <= 0:
raise ValueError("max_export_batch_size must be a positive integer.")
if max_export_batch_size > max_queue_size:
raise ValueError(
"max_export_batch_size must be less than or equal to max_queue_size."
)
@@ -0,0 +1,51 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import threading
import typing
from mysql.opentelemetry.sdk._logs import LogData
from mysql.opentelemetry.sdk._logs.export import LogExporter, LogExportResult
class InMemoryLogExporter(LogExporter):
"""Implementation of :class:`.LogExporter` that stores logs in memory.
This class can be used for testing purposes. It stores the exported logs
in a list in memory that can be retrieved using the
:func:`.get_finished_logs` method.
"""
def __init__(self):
self._logs = []
self._lock = threading.Lock()
self._stopped = False
def clear(self) -> None:
with self._lock:
self._logs.clear()
def get_finished_logs(self) -> typing.Tuple[LogData, ...]:
with self._lock:
return tuple(self._logs)
def export(self, batch: typing.Sequence[LogData]) -> LogExportResult:
if self._stopped:
return LogExportResult.FAILURE
with self._lock:
self._logs.extend(batch)
return LogExportResult.SUCCESS
def shutdown(self) -> None:
self._stopped = True
@@ -0,0 +1,35 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from mysql.opentelemetry.sdk._logs._internal.export import (
BatchLogRecordProcessor,
ConsoleLogExporter,
LogExporter,
LogExportResult,
SimpleLogRecordProcessor,
)
# The point module is not in the export directory to avoid a circular import.
from mysql.opentelemetry.sdk._logs._internal.export.in_memory_log_exporter import (
InMemoryLogExporter,
)
__all__ = [
"BatchLogRecordProcessor",
"ConsoleLogExporter",
"LogExporter",
"LogExportResult",
"SimpleLogRecordProcessor",
"InMemoryLogExporter",
]
@@ -0,0 +1,670 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
OTEL_RESOURCE_ATTRIBUTES = "OTEL_RESOURCE_ATTRIBUTES"
"""
.. envvar:: OTEL_RESOURCE_ATTRIBUTES
The :envvar:`OTEL_RESOURCE_ATTRIBUTES` environment variable allows resource
attributes to be passed to the SDK at process invocation. The attributes from
:envvar:`OTEL_RESOURCE_ATTRIBUTES` are merged with those passed to
`Resource.create`, meaning :envvar:`OTEL_RESOURCE_ATTRIBUTES` takes *lower*
priority. Attributes should be in the format ``key1=value1,key2=value2``.
Additional details are available `in the specification
<https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md#specifying-resource-information-via-an-environment-variable>`__.
.. code-block:: console
$ OTEL_RESOURCE_ATTRIBUTES="service.name=shoppingcard,will_be_overridden=foo" python - <<EOF
import pprint
from opentelemetry.sdk.resources import Resource
pprint.pprint(Resource.create({"will_be_overridden": "bar"}).attributes)
EOF
{'service.name': 'shoppingcard',
'telemetry.sdk.language': 'python',
'telemetry.sdk.name': 'opentelemetry',
'telemetry.sdk.version': '0.13.dev0',
'will_be_overridden': 'bar'}
"""
OTEL_LOG_LEVEL = "OTEL_LOG_LEVEL"
"""
.. envvar:: OTEL_LOG_LEVEL
The :envvar:`OTEL_LOG_LEVEL` environment variable sets the log level used by the SDK logger
Default: "info"
"""
OTEL_TRACES_SAMPLER = "OTEL_TRACES_SAMPLER"
"""
.. envvar:: OTEL_TRACES_SAMPLER
The :envvar:`OTEL_TRACES_SAMPLER` environment variable sets the sampler to be used for traces.
Sampling is a mechanism to control the noise introduced by OpenTelemetry by reducing the number
of traces collected and sent to the backend
Default: "parentbased_always_on"
"""
OTEL_TRACES_SAMPLER_ARG = "OTEL_TRACES_SAMPLER_ARG"
"""
.. envvar:: OTEL_TRACES_SAMPLER_ARG
The :envvar:`OTEL_TRACES_SAMPLER_ARG` environment variable will only be used if OTEL_TRACES_SAMPLER is set.
Each Sampler type defines its own expected input, if any.
Invalid or unrecognized input is ignored,
i.e. the SDK behaves as if OTEL_TRACES_SAMPLER_ARG is not set.
"""
OTEL_BLRP_SCHEDULE_DELAY = "OTEL_BLRP_SCHEDULE_DELAY"
"""
.. envvar:: OTEL_BLRP_SCHEDULE_DELAY
The :envvar:`OTEL_BLRP_SCHEDULE_DELAY` represents the delay interval between two consecutive exports of the BatchLogRecordProcessor.
Default: 5000
"""
OTEL_BLRP_EXPORT_TIMEOUT = "OTEL_BLRP_EXPORT_TIMEOUT"
"""
.. envvar:: OTEL_BLRP_EXPORT_TIMEOUT
The :envvar:`OTEL_BLRP_EXPORT_TIMEOUT` represents the maximum allowed time to export data from the BatchLogRecordProcessor.
Default: 30000
"""
OTEL_BLRP_MAX_QUEUE_SIZE = "OTEL_BLRP_MAX_QUEUE_SIZE"
"""
.. envvar:: OTEL_BLRP_MAX_QUEUE_SIZE
The :envvar:`OTEL_BLRP_MAX_QUEUE_SIZE` represents the maximum queue size for the data export of the BatchLogRecordProcessor.
Default: 2048
"""
OTEL_BLRP_MAX_EXPORT_BATCH_SIZE = "OTEL_BLRP_MAX_EXPORT_BATCH_SIZE"
"""
.. envvar:: OTEL_BLRP_MAX_EXPORT_BATCH_SIZE
The :envvar:`OTEL_BLRP_MAX_EXPORT_BATCH_SIZE` represents the maximum batch size for the data export of the BatchLogRecordProcessor.
Default: 512
"""
OTEL_BSP_SCHEDULE_DELAY = "OTEL_BSP_SCHEDULE_DELAY"
"""
.. envvar:: OTEL_BSP_SCHEDULE_DELAY
The :envvar:`OTEL_BSP_SCHEDULE_DELAY` represents the delay interval between two consecutive exports of the BatchSpanProcessor.
Default: 5000
"""
OTEL_BSP_EXPORT_TIMEOUT = "OTEL_BSP_EXPORT_TIMEOUT"
"""
.. envvar:: OTEL_BSP_EXPORT_TIMEOUT
The :envvar:`OTEL_BSP_EXPORT_TIMEOUT` represents the maximum allowed time to export data from the BatchSpanProcessor.
Default: 30000
"""
OTEL_BSP_MAX_QUEUE_SIZE = "OTEL_BSP_MAX_QUEUE_SIZE"
"""
.. envvar:: OTEL_BSP_MAX_QUEUE_SIZE
The :envvar:`OTEL_BSP_MAX_QUEUE_SIZE` represents the maximum queue size for the data export of the BatchSpanProcessor.
Default: 2048
"""
OTEL_BSP_MAX_EXPORT_BATCH_SIZE = "OTEL_BSP_MAX_EXPORT_BATCH_SIZE"
"""
.. envvar:: OTEL_BSP_MAX_EXPORT_BATCH_SIZE
The :envvar:`OTEL_BSP_MAX_EXPORT_BATCH_SIZE` represents the maximum batch size for the data export of the BatchSpanProcessor.
Default: 512
"""
OTEL_ATTRIBUTE_COUNT_LIMIT = "OTEL_ATTRIBUTE_COUNT_LIMIT"
"""
.. envvar:: OTEL_ATTRIBUTE_COUNT_LIMIT
The :envvar:`OTEL_ATTRIBUTE_COUNT_LIMIT` represents the maximum allowed attribute count for spans, events and links.
This limit is overridden by model specific limits such as OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT.
Default: 128
"""
OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT = "OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT"
"""
.. envvar:: OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT
The :envvar:`OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT` represents the maximum allowed attribute length.
"""
OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT = "OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT"
"""
.. envvar:: OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT
The :envvar:`OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT` represents the maximum allowed event attribute count.
Default: 128
"""
OTEL_LINK_ATTRIBUTE_COUNT_LIMIT = "OTEL_LINK_ATTRIBUTE_COUNT_LIMIT"
"""
.. envvar:: OTEL_LINK_ATTRIBUTE_COUNT_LIMIT
The :envvar:`OTEL_LINK_ATTRIBUTE_COUNT_LIMIT` represents the maximum allowed link attribute count.
Default: 128
"""
OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT = "OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT"
"""
.. envvar:: OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT
The :envvar:`OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT` represents the maximum allowed span attribute count.
Default: 128
"""
OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT = "OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT"
"""
.. envvar:: OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT
The :envvar:`OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` represents the maximum allowed length
span attribute values can have. This takes precedence over :envvar:`OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT`.
"""
OTEL_SPAN_EVENT_COUNT_LIMIT = "OTEL_SPAN_EVENT_COUNT_LIMIT"
"""
.. envvar:: OTEL_SPAN_EVENT_COUNT_LIMIT
The :envvar:`OTEL_SPAN_EVENT_COUNT_LIMIT` represents the maximum allowed span event count.
Default: 128
"""
OTEL_SPAN_LINK_COUNT_LIMIT = "OTEL_SPAN_LINK_COUNT_LIMIT"
"""
.. envvar:: OTEL_SPAN_LINK_COUNT_LIMIT
The :envvar:`OTEL_SPAN_LINK_COUNT_LIMIT` represents the maximum allowed span link count.
Default: 128
"""
OTEL_EXPORTER_JAEGER_AGENT_HOST = "OTEL_EXPORTER_JAEGER_AGENT_HOST"
"""
.. envvar:: OTEL_EXPORTER_JAEGER_AGENT_HOST
The :envvar:`OTEL_EXPORTER_JAEGER_AGENT_HOST` represents the hostname for the Jaeger agent.
Default: "localhost"
"""
OTEL_EXPORTER_JAEGER_AGENT_PORT = "OTEL_EXPORTER_JAEGER_AGENT_PORT"
"""
.. envvar:: OTEL_EXPORTER_JAEGER_AGENT_PORT
The :envvar:`OTEL_EXPORTER_JAEGER_AGENT_PORT` represents the port for the Jaeger agent.
Default: 6831
"""
OTEL_EXPORTER_JAEGER_ENDPOINT = "OTEL_EXPORTER_JAEGER_ENDPOINT"
"""
.. envvar:: OTEL_EXPORTER_JAEGER_ENDPOINT
The :envvar:`OTEL_EXPORTER_JAEGER_ENDPOINT` represents the HTTP endpoint for Jaeger traces.
Default: "http://localhost:14250"
"""
OTEL_EXPORTER_JAEGER_USER = "OTEL_EXPORTER_JAEGER_USER"
"""
.. envvar:: OTEL_EXPORTER_JAEGER_USER
The :envvar:`OTEL_EXPORTER_JAEGER_USER` represents the username to be used for HTTP basic authentication.
"""
OTEL_EXPORTER_JAEGER_PASSWORD = "OTEL_EXPORTER_JAEGER_PASSWORD"
"""
.. envvar:: OTEL_EXPORTER_JAEGER_PASSWORD
The :envvar:`OTEL_EXPORTER_JAEGER_PASSWORD` represents the password to be used for HTTP basic authentication.
"""
OTEL_EXPORTER_JAEGER_TIMEOUT = "OTEL_EXPORTER_JAEGER_TIMEOUT"
"""
.. envvar:: OTEL_EXPORTER_JAEGER_TIMEOUT
Maximum time the Jaeger exporter will wait for each batch export.
Default: 10
"""
OTEL_EXPORTER_ZIPKIN_ENDPOINT = "OTEL_EXPORTER_ZIPKIN_ENDPOINT"
"""
.. envvar:: OTEL_EXPORTER_ZIPKIN_ENDPOINT
Zipkin collector endpoint to which the exporter will send data. This may
include a path (e.g. ``http://example.com:9411/api/v2/spans``).
"""
OTEL_EXPORTER_ZIPKIN_TIMEOUT = "OTEL_EXPORTER_ZIPKIN_TIMEOUT"
"""
.. envvar:: OTEL_EXPORTER_ZIPKIN_TIMEOUT
Maximum time (in seconds) the Zipkin exporter will wait for each batch export.
Default: 10
"""
OTEL_EXPORTER_OTLP_PROTOCOL = "OTEL_EXPORTER_OTLP_PROTOCOL"
"""
.. envvar:: OTEL_EXPORTER_OTLP_PROTOCOL
The :envvar:`OTEL_EXPORTER_OTLP_PROTOCOL` represents the the transport protocol for the
OTLP exporter.
"""
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL"
"""
.. envvar:: OTEL_EXPORTER_OTLP_TRACES_PROTOCOL
The :envvar:`OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` represents the the transport protocol for spans.
"""
OTEL_EXPORTER_OTLP_METRICS_PROTOCOL = "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL"
"""
.. envvar:: OTEL_EXPORTER_OTLP_METRICS_PROTOCOL
The :envvar:`OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` represents the the transport protocol for metrics.
"""
OTEL_EXPORTER_OTLP_LOGS_PROTOCOL = "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL"
"""
.. envvar:: OTEL_EXPORTER_OTLP_LOGS_PROTOCOL
The :envvar:`OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` represents the the transport protocol for logs.
"""
OTEL_EXPORTER_OTLP_CERTIFICATE = "OTEL_EXPORTER_OTLP_CERTIFICATE"
"""
.. envvar:: OTEL_EXPORTER_OTLP_CERTIFICATE
The :envvar:`OTEL_EXPORTER_OTLP_CERTIFICATE` stores the path to the certificate file for
TLS credentials of gRPC client. Should only be used for a secure connection.
"""
OTEL_EXPORTER_OTLP_HEADERS = "OTEL_EXPORTER_OTLP_HEADERS"
"""
.. envvar:: OTEL_EXPORTER_OTLP_HEADERS
The :envvar:`OTEL_EXPORTER_OTLP_HEADERS` contains the key-value pairs to be used as headers
associated with gRPC or HTTP requests.
"""
OTEL_EXPORTER_OTLP_COMPRESSION = "OTEL_EXPORTER_OTLP_COMPRESSION"
"""
.. envvar:: OTEL_EXPORTER_OTLP_COMPRESSION
Specifies a gRPC compression method to be used in the OTLP exporters.
Possible values are:
- ``gzip`` corresponding to `grpc.Compression.Gzip`.
- ``deflate`` corresponding to `grpc.Compression.Deflate`.
If no ``OTEL_EXPORTER_OTLP_*COMPRESSION`` environment variable is present or
``compression`` argument passed to the exporter, the default
`grpc.Compression.NoCompression` will be used. Additional details are
available `in the specification
<https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#opentelemetry-protocol-exporter>`__.
"""
OTEL_EXPORTER_OTLP_TIMEOUT = "OTEL_EXPORTER_OTLP_TIMEOUT"
"""
.. envvar:: OTEL_EXPORTER_OTLP_TIMEOUT
The :envvar:`OTEL_EXPORTER_OTLP_TIMEOUT` is the maximum time the OTLP exporter will wait for each batch export.
Default: 10
"""
OTEL_EXPORTER_OTLP_ENDPOINT = "OTEL_EXPORTER_OTLP_ENDPOINT"
"""
.. envvar:: OTEL_EXPORTER_OTLP_ENDPOINT
The :envvar:`OTEL_EXPORTER_OTLP_ENDPOINT` target to which the exporter is going to send spans or metrics.
The endpoint MUST be a valid URL host, and MAY contain a scheme (http or https), port and path.
A scheme of https indicates a secure connection and takes precedence over the insecure configuration setting.
Default: "http://localhost:4317"
"""
OTEL_EXPORTER_OTLP_INSECURE = "OTEL_EXPORTER_OTLP_INSECURE"
"""
.. envvar:: OTEL_EXPORTER_OTLP_INSECURE
The :envvar:`OTEL_EXPORTER_OTLP_INSECURE` represents whether to enable client transport security for gRPC requests.
A scheme of https takes precedence over this configuration setting.
Default: False
"""
OTEL_EXPORTER_OTLP_TRACES_INSECURE = "OTEL_EXPORTER_OTLP_TRACES_INSECURE"
"""
.. envvar:: OTEL_EXPORTER_OTLP_TRACES_INSECURE
The :envvar:`OTEL_EXPORTER_OTLP_TRACES_INSECURE` represents whether to enable client transport security
for gRPC requests for spans. A scheme of https takes precedence over the this configuration setting.
Default: False
"""
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"
"""
.. envvar:: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
The :envvar:`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` target to which the span exporter is going to send spans.
The endpoint MUST be a valid URL host, and MAY contain a scheme (http or https), port and path.
A scheme of https indicates a secure connection and takes precedence over this configuration setting.
"""
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT"
"""
.. envvar:: OTEL_EXPORTER_OTLP_METRICS_ENDPOINT
The :envvar:`OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` target to which the metrics exporter is going to send metrics.
The endpoint MUST be a valid URL host, and MAY contain a scheme (http or https), port and path.
A scheme of https indicates a secure connection and takes precedence over this configuration setting.
"""
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT"
"""
.. envvar:: OTEL_EXPORTER_OTLP_LOGS_ENDPOINT
The :envvar:`OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` target to which the log exporter is going to send logs.
The endpoint MUST be a valid URL host, and MAY contain a scheme (http or https), port and path.
A scheme of https indicates a secure connection and takes precedence over this configuration setting.
"""
OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE = "OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE"
"""
.. envvar:: OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE
The :envvar:`OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE` stores the path to the certificate file for
TLS credentials of gRPC client for traces. Should only be used for a secure connection for tracing.
"""
OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE = "OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE"
"""
.. envvar:: OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE
The :envvar:`OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE` stores the path to the certificate file for
TLS credentials of gRPC client for metrics. Should only be used for a secure connection for exporting metrics.
"""
OTEL_EXPORTER_OTLP_TRACES_HEADERS = "OTEL_EXPORTER_OTLP_TRACES_HEADERS"
"""
.. envvar:: OTEL_EXPORTER_OTLP_TRACES_HEADERS
The :envvar:`OTEL_EXPORTER_OTLP_TRACES_HEADERS` contains the key-value pairs to be used as headers for spans
associated with gRPC or HTTP requests.
"""
OTEL_EXPORTER_OTLP_METRICS_HEADERS = "OTEL_EXPORTER_OTLP_METRICS_HEADERS"
"""
.. envvar:: OTEL_EXPORTER_OTLP_METRICS_HEADERS
The :envvar:`OTEL_EXPORTER_OTLP_METRICS_HEADERS` contains the key-value pairs to be used as headers for metrics
associated with gRPC or HTTP requests.
"""
OTEL_EXPORTER_OTLP_LOGS_HEADERS = "OTEL_EXPORTER_OTLP_LOGS_HEADERS"
"""
.. envvar:: OTEL_EXPORTER_OTLP_LOGS_HEADERS
The :envvar:`OTEL_EXPORTER_OTLP_LOGS_HEADERS` contains the key-value pairs to be used as headers for logs
associated with gRPC or HTTP requests.
"""
OTEL_EXPORTER_OTLP_TRACES_COMPRESSION = "OTEL_EXPORTER_OTLP_TRACES_COMPRESSION"
"""
.. envvar:: OTEL_EXPORTER_OTLP_TRACES_COMPRESSION
Same as :envvar:`OTEL_EXPORTER_OTLP_COMPRESSION` but only for the span
exporter. If both are present, this takes higher precedence.
"""
OTEL_EXPORTER_OTLP_METRICS_COMPRESSION = "OTEL_EXPORTER_OTLP_METRICS_COMPRESSION"
"""
.. envvar:: OTEL_EXPORTER_OTLP_METRICS_COMPRESSION
Same as :envvar:`OTEL_EXPORTER_OTLP_COMPRESSION` but only for the metric
exporter. If both are present, this takes higher precedence.
"""
OTEL_EXPORTER_OTLP_LOGS_COMPRESSION = "OTEL_EXPORTER_OTLP_LOGS_COMPRESSION"
"""
.. envvar:: OTEL_EXPORTER_OTLP_LOGS_COMPRESSION
Same as :envvar:`OTEL_EXPORTER_OTLP_COMPRESSION` but only for the log
exporter. If both are present, this takes higher precedence.
"""
OTEL_EXPORTER_OTLP_TRACES_TIMEOUT = "OTEL_EXPORTER_OTLP_TRACES_TIMEOUT"
"""
.. envvar:: OTEL_EXPORTER_OTLP_TRACES_TIMEOUT
The :envvar:`OTEL_EXPORTER_OTLP_TRACES_TIMEOUT` is the maximum time the OTLP exporter will
wait for each batch export for spans.
"""
OTEL_EXPORTER_OTLP_METRICS_TIMEOUT = "OTEL_EXPORTER_OTLP_METRICS_TIMEOUT"
"""
.. envvar:: OTEL_EXPORTER_OTLP_METRICS_TIMEOUT
The :envvar:`OTEL_EXPORTER_OTLP_METRICS_TIMEOUT` is the maximum time the OTLP exporter will
wait for each batch export for metrics.
"""
OTEL_EXPORTER_OTLP_METRICS_INSECURE = "OTEL_EXPORTER_OTLP_METRICS_INSECURE"
"""
.. envvar:: OTEL_EXPORTER_OTLP_METRICS_INSECURE
The :envvar:`OTEL_EXPORTER_OTLP_METRICS_INSECURE` represents whether to enable client transport security
for gRPC requests for metrics. A scheme of https takes precedence over the this configuration setting.
Default: False
"""
OTEL_EXPORTER_OTLP_LOGS_INSECURE = "OTEL_EXPORTER_OTLP_LOGS_INSECURE"
"""
.. envvar:: OTEL_EXPORTER_OTLP_LOGS_INSECURE
The :envvar:`OTEL_EXPORTER_OTLP_LOGS_INSECURE` represents whether to enable client transport security
for gRPC requests for metrics. A scheme of https takes precedence over the this configuration setting.
Default: False
"""
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT"
"""
.. envvar:: OTEL_EXPORTER_OTLP_METRICS_ENDPOINT
The :envvar:`OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` target to which the metric exporter is going to send spans.
The endpoint MUST be a valid URL host, and MAY contain a scheme (http or https), port and path.
A scheme of https indicates a secure connection and takes precedence over this configuration setting.
"""
OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE = "OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE"
"""
.. envvar:: OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE
The :envvar:`OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE` stores the path to the certificate file for
TLS credentials of gRPC client for traces. Should only be used for a secure connection for tracing.
"""
OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE = "OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE"
"""
.. envvar:: OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE
The :envvar:`OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE` stores the path to the certificate file for
TLS credentials of gRPC client for traces. Should only be used for a secure connection for tracing.
"""
OTEL_EXPORTER_OTLP_METRICS_HEADERS = "OTEL_EXPORTER_OTLP_METRICS_HEADERS"
"""
.. envvar:: OTEL_EXPORTER_OTLP_METRICS_HEADERS
The :envvar:`OTEL_EXPORTER_OTLP_METRICS_HEADERS` contains the key-value pairs to be used as headers for metrics
associated with gRPC or HTTP requests.
"""
OTEL_EXPORTER_OTLP_METRICS_TIMEOUT = "OTEL_EXPORTER_OTLP_METRICS_TIMEOUT"
"""
.. envvar:: OTEL_EXPORTER_OTLP_METRICS_TIMEOUT
The :envvar:`OTEL_EXPORTER_OTLP_METRICS_TIMEOUT` is the maximum time the OTLP exporter will
wait for each batch export for metrics.
"""
OTEL_EXPORTER_OTLP_LOGS_TIMEOUT = "OTEL_EXPORTER_OTLP_LOGS_TIMEOUT"
"""
.. envvar:: OTEL_EXPORTER_OTLP_LOGS_TIMEOUT
The :envvar:`OTEL_EXPORTER_OTLP_LOGS_TIMEOUT` is the maximum time the OTLP exporter will
wait for each batch export for logs.
"""
OTEL_EXPORTER_OTLP_METRICS_COMPRESSION = "OTEL_EXPORTER_OTLP_METRICS_COMPRESSION"
"""
.. envvar:: OTEL_EXPORTER_OTLP_METRICS_COMPRESSION
Same as :envvar:`OTEL_EXPORTER_OTLP_COMPRESSION` but only for the metric
exporter. If both are present, this takes higher precedence.
"""
OTEL_EXPORTER_JAEGER_CERTIFICATE = "OTEL_EXPORTER_JAEGER_CERTIFICATE"
"""
.. envvar:: OTEL_EXPORTER_JAEGER_CERTIFICATE
The :envvar:`OTEL_EXPORTER_JAEGER_CERTIFICATE` stores the path to the certificate file for
TLS credentials of gRPC client for Jaeger. Should only be used for a secure connection with Jaeger.
"""
OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES = (
"OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES"
)
"""
.. envvar:: OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES
The :envvar:`OTEL_EXPORTER_JAEGER_AGENT_SPLIT_OVERSIZED_BATCHES` is a boolean flag to determine whether
to split a large span batch to admire the udp packet size limit.
"""
OTEL_SERVICE_NAME = "OTEL_SERVICE_NAME"
"""
.. envvar:: OTEL_SERVICE_NAME
Convenience environment variable for setting the service name resource attribute.
The following two environment variables have the same effect
.. code-block:: console
OTEL_SERVICE_NAME=my-python-service
OTEL_RESOURCE_ATTRIBUTES=service.name=my-python-service
If both are set, :envvar:`OTEL_SERVICE_NAME` takes precedence.
"""
_OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED = (
"OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED"
)
"""
.. envvar:: OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED
The :envvar:`OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED` environment variable allows users to
enable/disable the logging SDK auto instrumentation.
Default: False
Note: Logs SDK and its related settings are experimental.
"""
OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE = (
"OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE"
)
"""
.. envvar:: OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE
The :envvar:`OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` environment
variable allows users to set the default aggregation temporality policy to use
on the basis of instrument kind. The valid (case-insensitive) values are:
``CUMULATIVE``: Use ``CUMULATIVE`` aggregation temporality for all instrument kinds.
``DELTA``: Use ``DELTA`` aggregation temporality for ``Counter``, ``Asynchronous Counter`` and ``Histogram``.
Use ``CUMULATIVE`` aggregation temporality for ``UpDownCounter`` and ``Asynchronous UpDownCounter``.
``LOWMEMORY``: Use ``DELTA`` aggregation temporality for ``Counter`` and ``Histogram``.
Use ``CUMULATIVE`` aggregation temporality for ``UpDownCounter``, ``AsynchronousCounter`` and ``Asynchronous UpDownCounter``.
"""
OTEL_EXPORTER_JAEGER_GRPC_INSECURE = "OTEL_EXPORTER_JAEGER_GRPC_INSECURE"
"""
.. envvar:: OTEL_EXPORTER_JAEGER_GRPC_INSECURE
The :envvar:`OTEL_EXPORTER_JAEGER_GRPC_INSECURE` is a boolean flag to True if collector has no encryption or authentication.
"""
OTEL_METRIC_EXPORT_INTERVAL = "OTEL_METRIC_EXPORT_INTERVAL"
"""
.. envvar:: OTEL_METRIC_EXPORT_INTERVAL
The :envvar:`OTEL_METRIC_EXPORT_INTERVAL` is the time interval (in milliseconds) between the start of two export attempts.
"""
OTEL_METRIC_EXPORT_TIMEOUT = "OTEL_METRIC_EXPORT_TIMEOUT"
"""
.. envvar:: OTEL_METRIC_EXPORT_TIMEOUT
The :envvar:`OTEL_METRIC_EXPORT_TIMEOUT` is the maximum allowed time (in milliseconds) to export data.
"""
OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY = "OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY"
"""
.. envvar:: OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY
The :envvar:`OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY` is the clients private key to use in mTLS communication in PEM format.
"""
OTEL_METRICS_EXEMPLAR_FILTER = "OTEL_METRICS_EXEMPLAR_FILTER"
"""
.. envvar:: OTEL_METRICS_EXEMPLAR_FILTER
The :envvar:`OTEL_METRICS_EXEMPLAR_FILTER` is the filter for which measurements can become Exemplars.
"""
OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION = (
"OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION"
)
"""
.. envvar:: OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION
The :envvar:`OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION` is the default aggregation to use for histogram instruments.
"""
OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE = (
"OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE"
)
"""
.. envvar:: OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE
The :envvar:`OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE` is the client certificate/chain trust for clients private key to use in mTLS communication in PEM format.
"""
OTEL_EXPERIMENTAL_RESOURCE_DETECTORS = "OTEL_EXPERIMENTAL_RESOURCE_DETECTORS"
"""
.. envvar:: OTEL_EXPERIMENTAL_RESOURCE_DETECTORS
The :envvar:`OTEL_EXPERIMENTAL_RESOURCE_DETECTORS` is a comma-separated string
of names of resource detectors. These names must be the same as the names of
entry points for the `opentelemetry_resource_detector` entry point. This is an
experimental feature and the name of this variable and its behavior can change
in a non-backwards compatible way.
"""
@@ -0,0 +1,140 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Global Error Handler
This module provides a global error handler and an interface that allows
error handlers to be registered with the global error handler via entry points.
A default error handler is also provided.
To use this feature, users can create an error handler that is registered
using the ``opentelemetry_error_handler`` entry point. A class is to be
registered in this entry point, this class must inherit from the
``opentelemetry.sdk.error_handler.ErrorHandler`` class and implement the
corresponding ``handle`` method. This method will receive the exception object
that is to be handled. The error handler class should also inherit from the
exception classes it wants to handle. For example, this would be an error
handler that handles ``ZeroDivisionError``:
.. code:: python
from opentelemetry.sdk.error_handler import ErrorHandler
from logging import getLogger
logger = getLogger(__name__)
class ErrorHandler0(ErrorHandler, ZeroDivisionError):
def _handle(self, error: Exception, *args, **kwargs):
logger.exception("ErrorHandler0 handling a ZeroDivisionError")
To use the global error handler, just instantiate it as a context manager where
you want exceptions to be handled:
.. code:: python
from opentelemetry.sdk.error_handler import GlobalErrorHandler
with GlobalErrorHandler():
1 / 0
If the class of the exception raised in the scope of the ``GlobalErrorHandler``
object is not parent of any registered error handler, then the default error
handler will handle the exception. This default error handler will only log the
exception to standard logging, the exception won't be raised any further.
"""
from abc import ABC, abstractmethod
from logging import getLogger
from mysql.opentelemetry.util._importlib_metadata import entry_points
logger = getLogger(__name__)
class ErrorHandler(ABC):
@abstractmethod
def _handle(self, error: Exception, *args, **kwargs):
"""
Handle an exception
"""
class _DefaultErrorHandler(ErrorHandler):
"""
Default error handler
This error handler just logs the exception using standard logging.
"""
# pylint: disable=useless-return
def _handle(self, error: Exception, *args, **kwargs):
logger.exception("Error handled by default error handler: ")
return None
class GlobalErrorHandler:
"""
Global error handler
This is a singleton class that can be instantiated anywhere to get the
global error handler. This object provides a handle method that receives
an exception object that will be handled by the registered error handlers.
"""
_instance = None
def __new__(cls) -> "GlobalErrorHandler":
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __enter__(self):
pass
# pylint: disable=no-self-use
def __exit__(self, exc_type, exc_value, traceback):
if exc_value is None:
return None
plugin_handled = False
error_handler_entry_points = entry_points(group="opentelemetry_error_handler")
for error_handler_entry_point in error_handler_entry_points:
error_handler_class = error_handler_entry_point.load()
if issubclass(error_handler_class, exc_value.__class__):
try:
error_handler_class()._handle(exc_value)
plugin_handled = True
# pylint: disable=broad-except
except Exception as error_handling_error:
logger.exception(
"%s error while handling error" " %s by error handler %s",
error_handling_error.__class__.__name__,
exc_value.__class__.__name__,
error_handler_class.__name__,
)
if not plugin_handled:
_DefaultErrorHandler()._handle(exc_value)
return True
@@ -0,0 +1,37 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from mysql.opentelemetry.sdk.metrics._internal import Meter, MeterProvider
from mysql.opentelemetry.sdk.metrics._internal.exceptions import MetricsTimeoutError
from mysql.opentelemetry.sdk.metrics._internal.instrument import (
Counter,
Histogram,
ObservableCounter,
ObservableGauge,
ObservableUpDownCounter,
UpDownCounter,
)
__all__ = [
"Meter",
"MeterProvider",
"MetricsTimeoutError",
"Counter",
"Histogram",
"ObservableCounter",
"ObservableGauge",
"ObservableUpDownCounter",
"UpDownCounter",
]
@@ -0,0 +1,467 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from atexit import register, unregister
from logging import getLogger
from threading import Lock
from time import time_ns
from typing import Optional, Sequence
# This kind of import is needed to avoid Sphinx errors.
import mysql.opentelemetry.sdk.metrics
from mysql.opentelemetry.metrics import (
Counter as APICounter,
Histogram as APIHistogram,
Meter as APIMeter,
MeterProvider as APIMeterProvider,
NoOpMeter,
ObservableCounter as APIObservableCounter,
ObservableGauge as APIObservableGauge,
ObservableUpDownCounter as APIObservableUpDownCounter,
UpDownCounter as APIUpDownCounter,
)
from mysql.opentelemetry.sdk.metrics._internal.exceptions import MetricsTimeoutError
from mysql.opentelemetry.sdk.metrics._internal.instrument import (
_Counter,
_Histogram,
_ObservableCounter,
_ObservableGauge,
_ObservableUpDownCounter,
_UpDownCounter,
)
from mysql.opentelemetry.sdk.metrics._internal.measurement_consumer import (
MeasurementConsumer,
SynchronousMeasurementConsumer,
)
from mysql.opentelemetry.sdk.metrics._internal.sdk_configuration import SdkConfiguration
from mysql.opentelemetry.sdk.resources import Resource
from mysql.opentelemetry.sdk.util.instrumentation import InstrumentationScope
from mysql.opentelemetry.util._once import Once
_logger = getLogger(__name__)
class Meter(APIMeter):
"""See `mysql.opentelemetry.metrics.Meter`."""
def __init__(
self,
instrumentation_scope: InstrumentationScope,
measurement_consumer: MeasurementConsumer,
):
super().__init__(
name=instrumentation_scope.name,
version=instrumentation_scope.version,
schema_url=instrumentation_scope.schema_url,
)
self._instrumentation_scope = instrumentation_scope
self._measurement_consumer = measurement_consumer
self._instrument_id_instrument = {}
self._instrument_id_instrument_lock = Lock()
def create_counter(self, name, unit="", description="") -> APICounter:
(
is_instrument_registered,
instrument_id,
) = self._is_instrument_registered(name, _Counter, unit, description)
if is_instrument_registered:
# FIXME #2558 go through all views here and check if this
# instrument registration conflict can be fixed. If it can be, do
# not log the following warning.
_logger.warning(
"An instrument with name %s, type %s, unit %s and "
"description %s has been created already.",
name,
APICounter.__name__,
unit,
description,
)
with self._instrument_id_instrument_lock:
return self._instrument_id_instrument[instrument_id]
instrument = _Counter(
name,
self._instrumentation_scope,
self._measurement_consumer,
unit,
description,
)
with self._instrument_id_instrument_lock:
self._instrument_id_instrument[instrument_id] = instrument
return instrument
def create_up_down_counter(self, name, unit="", description="") -> APIUpDownCounter:
(
is_instrument_registered,
instrument_id,
) = self._is_instrument_registered(name, _UpDownCounter, unit, description)
if is_instrument_registered:
# FIXME #2558 go through all views here and check if this
# instrument registration conflict can be fixed. If it can be, do
# not log the following warning.
_logger.warning(
"An instrument with name %s, type %s, unit %s and "
"description %s has been created already.",
name,
APIUpDownCounter.__name__,
unit,
description,
)
with self._instrument_id_instrument_lock:
return self._instrument_id_instrument[instrument_id]
instrument = _UpDownCounter(
name,
self._instrumentation_scope,
self._measurement_consumer,
unit,
description,
)
with self._instrument_id_instrument_lock:
self._instrument_id_instrument[instrument_id] = instrument
return instrument
def create_observable_counter(
self, name, callbacks=None, unit="", description=""
) -> APIObservableCounter:
(
is_instrument_registered,
instrument_id,
) = self._is_instrument_registered(name, _ObservableCounter, unit, description)
if is_instrument_registered:
# FIXME #2558 go through all views here and check if this
# instrument registration conflict can be fixed. If it can be, do
# not log the following warning.
_logger.warning(
"An instrument with name %s, type %s, unit %s and "
"description %s has been created already.",
name,
APIObservableCounter.__name__,
unit,
description,
)
with self._instrument_id_instrument_lock:
return self._instrument_id_instrument[instrument_id]
instrument = _ObservableCounter(
name,
self._instrumentation_scope,
self._measurement_consumer,
callbacks,
unit,
description,
)
self._measurement_consumer.register_asynchronous_instrument(instrument)
with self._instrument_id_instrument_lock:
self._instrument_id_instrument[instrument_id] = instrument
return instrument
def create_histogram(self, name, unit="", description="") -> APIHistogram:
(
is_instrument_registered,
instrument_id,
) = self._is_instrument_registered(name, _Histogram, unit, description)
if is_instrument_registered:
# FIXME #2558 go through all views here and check if this
# instrument registration conflict can be fixed. If it can be, do
# not log the following warning.
_logger.warning(
"An instrument with name %s, type %s, unit %s and "
"description %s has been created already.",
name,
APIHistogram.__name__,
unit,
description,
)
with self._instrument_id_instrument_lock:
return self._instrument_id_instrument[instrument_id]
instrument = _Histogram(
name,
self._instrumentation_scope,
self._measurement_consumer,
unit,
description,
)
with self._instrument_id_instrument_lock:
self._instrument_id_instrument[instrument_id] = instrument
return instrument
def create_observable_gauge(
self, name, callbacks=None, unit="", description=""
) -> APIObservableGauge:
(
is_instrument_registered,
instrument_id,
) = self._is_instrument_registered(name, _ObservableGauge, unit, description)
if is_instrument_registered:
# FIXME #2558 go through all views here and check if this
# instrument registration conflict can be fixed. If it can be, do
# not log the following warning.
_logger.warning(
"An instrument with name %s, type %s, unit %s and "
"description %s has been created already.",
name,
APIObservableGauge.__name__,
unit,
description,
)
with self._instrument_id_instrument_lock:
return self._instrument_id_instrument[instrument_id]
instrument = _ObservableGauge(
name,
self._instrumentation_scope,
self._measurement_consumer,
callbacks,
unit,
description,
)
self._measurement_consumer.register_asynchronous_instrument(instrument)
with self._instrument_id_instrument_lock:
self._instrument_id_instrument[instrument_id] = instrument
return instrument
def create_observable_up_down_counter(
self, name, callbacks=None, unit="", description=""
) -> APIObservableUpDownCounter:
(
is_instrument_registered,
instrument_id,
) = self._is_instrument_registered(
name, _ObservableUpDownCounter, unit, description
)
if is_instrument_registered:
# FIXME #2558 go through all views here and check if this
# instrument registration conflict can be fixed. If it can be, do
# not log the following warning.
_logger.warning(
"An instrument with name %s, type %s, unit %s and "
"description %s has been created already.",
name,
APIObservableUpDownCounter.__name__,
unit,
description,
)
with self._instrument_id_instrument_lock:
return self._instrument_id_instrument[instrument_id]
instrument = _ObservableUpDownCounter(
name,
self._instrumentation_scope,
self._measurement_consumer,
callbacks,
unit,
description,
)
self._measurement_consumer.register_asynchronous_instrument(instrument)
with self._instrument_id_instrument_lock:
self._instrument_id_instrument[instrument_id] = instrument
return instrument
class MeterProvider(APIMeterProvider):
r"""See `mysql.opentelemetry.metrics.MeterProvider`.
Args:
metric_readers: Register metric readers to collect metrics from the SDK
on demand. Each :class:`mysql.opentelemetry.sdk.metrics.export.MetricReader` is
completely independent and will collect separate streams of
metrics. TODO: reference ``PeriodicExportingMetricReader`` usage with push
exporters here.
resource: The resource representing what the metrics emitted from the SDK pertain to.
shutdown_on_exit: If true, registers an `atexit` handler to call
`MeterProvider.shutdown`
views: The views to configure the metric output the SDK
By default, instruments which do not match any :class:`mysql.opentelemetry.sdk.metrics.view.View` (or if no :class:`mysql.opentelemetry.sdk.metrics.view.View`\ s
are provided) will report metrics with the default aggregation for the
instrument's kind. To disable instruments by default, configure a match-all
:class:`mysql.opentelemetry.sdk.metrics.view.View` with `DropAggregation` and then create :class:`mysql.opentelemetry.sdk.metrics.view.View`\ s to re-enable
individual instruments:
.. code-block:: python
:caption: Disable default views
MeterProvider(
views=[
View(instrument_name="*", aggregation=DropAggregation()),
View(instrument_name="mycounter"),
],
# ...
)
"""
_all_metric_readers_lock = Lock()
_all_metric_readers = set()
def __init__(
self,
metric_readers: Sequence[
"mysql.opentelemetry.sdk.metrics.export.MetricReader"
] = (),
resource: Resource = Resource.create({}),
shutdown_on_exit: bool = True,
views: Sequence["mysql.opentelemetry.sdk.metrics.view.View"] = (),
):
self._lock = Lock()
self._meter_lock = Lock()
self._atexit_handler = None
self._sdk_config = SdkConfiguration(
resource=resource,
metric_readers=metric_readers,
views=views,
)
self._measurement_consumer = SynchronousMeasurementConsumer(
sdk_config=self._sdk_config
)
if shutdown_on_exit:
self._atexit_handler = register(self.shutdown)
self._meters = {}
self._shutdown_once = Once()
self._shutdown = False
for metric_reader in self._sdk_config.metric_readers:
with self._all_metric_readers_lock:
if metric_reader in self._all_metric_readers:
raise Exception(
f"MetricReader {metric_reader} has been registered "
"already in other MeterProvider instance"
)
self._all_metric_readers.add(metric_reader)
metric_reader._set_collect_callback(self._measurement_consumer.collect)
def force_flush(self, timeout_millis: float = 10_000) -> bool:
deadline_ns = time_ns() + timeout_millis * 10**6
metric_reader_error = {}
for metric_reader in self._sdk_config.metric_readers:
current_ts = time_ns()
try:
if current_ts >= deadline_ns:
raise MetricsTimeoutError("Timed out while flushing metric readers")
metric_reader.force_flush(
timeout_millis=(deadline_ns - current_ts) / 10**6
)
# pylint: disable=broad-except
except Exception as error:
metric_reader_error[metric_reader] = error
if metric_reader_error:
metric_reader_error_string = "\n".join(
[
f"{metric_reader.__class__.__name__}: {repr(error)}"
for metric_reader, error in metric_reader_error.items()
]
)
raise Exception(
"MeterProvider.force_flush failed because the following "
"metric readers failed during collect:\n"
f"{metric_reader_error_string}"
)
return True
def shutdown(self, timeout_millis: float = 30_000):
deadline_ns = time_ns() + timeout_millis * 10**6
def _shutdown():
self._shutdown = True
did_shutdown = self._shutdown_once.do_once(_shutdown)
if not did_shutdown:
_logger.warning("shutdown can only be called once")
return
metric_reader_error = {}
for metric_reader in self._sdk_config.metric_readers:
current_ts = time_ns()
try:
if current_ts >= deadline_ns:
raise Exception("Didn't get to execute, deadline already exceeded")
metric_reader.shutdown(
timeout_millis=(deadline_ns - current_ts) / 10**6
)
# pylint: disable=broad-except
except Exception as error:
metric_reader_error[metric_reader] = error
if self._atexit_handler is not None:
unregister(self._atexit_handler)
self._atexit_handler = None
if metric_reader_error:
metric_reader_error_string = "\n".join(
[
f"{metric_reader.__class__.__name__}: {repr(error)}"
for metric_reader, error in metric_reader_error.items()
]
)
raise Exception(
(
"MeterProvider.shutdown failed because the following "
"metric readers failed during shutdown:\n"
f"{metric_reader_error_string}"
)
)
def get_meter(
self,
name: str,
version: Optional[str] = None,
schema_url: Optional[str] = None,
) -> Meter:
if self._shutdown:
_logger.warning("A shutdown `MeterProvider` can not provide a `Meter`")
return NoOpMeter(name, version=version, schema_url=schema_url)
if not name:
_logger.warning("Meter name cannot be None or empty.")
return NoOpMeter(name, version=version, schema_url=schema_url)
info = InstrumentationScope(name, version, schema_url)
with self._meter_lock:
if not self._meters.get(info):
# FIXME #2558 pass SDKConfig object to meter so that the meter
# has access to views.
self._meters[info] = Meter(
info,
self._measurement_consumer,
)
return self._meters[info]
@@ -0,0 +1,130 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from logging import getLogger
from threading import Lock
from time import time_ns
from typing import Dict, List, Sequence
from mysql.opentelemetry.metrics import Instrument
from mysql.opentelemetry.sdk.metrics._internal.aggregation import (
Aggregation,
DefaultAggregation,
_Aggregation,
_SumAggregation,
)
from mysql.opentelemetry.sdk.metrics._internal.export import AggregationTemporality
from mysql.opentelemetry.sdk.metrics._internal.measurement import Measurement
from mysql.opentelemetry.sdk.metrics._internal.point import DataPointT
from mysql.opentelemetry.sdk.metrics._internal.view import View
_logger = getLogger(__name__)
class _ViewInstrumentMatch:
def __init__(
self,
view: View,
instrument: Instrument,
instrument_class_aggregation: Dict[type, Aggregation],
):
self._start_time_unix_nano = time_ns()
self._view = view
self._instrument = instrument
self._attributes_aggregation: Dict[frozenset, _Aggregation] = {}
self._lock = Lock()
self._instrument_class_aggregation = instrument_class_aggregation
self._name = self._view._name or self._instrument.name
self._description = self._view._description or self._instrument.description
if not isinstance(self._view._aggregation, DefaultAggregation):
self._aggregation = self._view._aggregation._create_aggregation(
self._instrument, None, 0
)
else:
self._aggregation = self._instrument_class_aggregation[
self._instrument.__class__
]._create_aggregation(self._instrument, None, 0)
def conflicts(self, other: "_ViewInstrumentMatch") -> bool:
# pylint: disable=protected-access
result = (
self._name == other._name
and self._instrument.unit == other._instrument.unit
# The aggregation class is being used here instead of data point
# type since they are functionally equivalent.
and self._aggregation.__class__ == other._aggregation.__class__
)
if isinstance(self._aggregation, _SumAggregation):
result = (
result
and self._aggregation._instrument_is_monotonic
== other._aggregation._instrument_is_monotonic
and self._aggregation._instrument_temporality
== other._aggregation._instrument_temporality
)
return result
# pylint: disable=protected-access
def consume_measurement(self, measurement: Measurement) -> None:
if self._view._attribute_keys is not None:
attributes = {}
for key, value in (measurement.attributes or {}).items():
if key in self._view._attribute_keys:
attributes[key] = value
elif measurement.attributes is not None:
attributes = measurement.attributes
else:
attributes = {}
aggr_key = frozenset(attributes.items())
if aggr_key not in self._attributes_aggregation:
with self._lock:
if aggr_key not in self._attributes_aggregation:
if not isinstance(self._view._aggregation, DefaultAggregation):
aggregation = self._view._aggregation._create_aggregation(
self._instrument,
attributes,
self._start_time_unix_nano,
)
else:
aggregation = self._instrument_class_aggregation[
self._instrument.__class__
]._create_aggregation(
self._instrument,
attributes,
self._start_time_unix_nano,
)
self._attributes_aggregation[aggr_key] = aggregation
self._attributes_aggregation[aggr_key].aggregate(measurement)
def collect(
self,
aggregation_temporality: AggregationTemporality,
collection_start_nanos: int,
) -> Sequence[DataPointT]:
data_points: List[DataPointT] = []
with self._lock:
for aggregation in self._attributes_aggregation.values():
data_point = aggregation.collect(
aggregation_temporality, collection_start_nanos
)
if data_point is not None:
data_points.append(data_point)
return data_points
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,17 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
class MetricsTimeoutError(Exception):
"""Raised when a metrics function times out"""
@@ -0,0 +1,170 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from math import ceil, log2
class Buckets:
# No method of this class is protected by locks because instances of this
# class are only used in methods that are protected by locks themselves.
def __init__(self):
self._counts = [0]
# The term index refers to the number of the exponential histogram bucket
# used to determine its boundaries. The lower boundary of a bucket is
# determined by base ** index and the upper boundary of a bucket is
# determined by base ** (index + 1). index values are signedto account
# for values less than or equal to 1.
# self._index_* will all have values equal to a certain index that is
# determined by the corresponding mapping _map_to_index function and
# the value of the index depends on the value passed to _map_to_index.
# Index of the 0th position in self._counts: self._counts[0] is the
# count in the bucket with index self.__index_base.
self.__index_base = 0
# self.__index_start is the smallest index value represented in
# self._counts.
self.__index_start = 0
# self.__index_start is the largest index value represented in
# self._counts.
self.__index_end = 0
@property
def index_start(self) -> int:
return self.__index_start
@index_start.setter
def index_start(self, value: int) -> None:
self.__index_start = value
@property
def index_end(self) -> int:
return self.__index_end
@index_end.setter
def index_end(self, value: int) -> None:
self.__index_end = value
@property
def index_base(self) -> int:
return self.__index_base
@index_base.setter
def index_base(self, value: int) -> None:
self.__index_base = value
@property
def counts(self):
return self._counts
def grow(self, needed: int, max_size: int) -> None:
size = len(self._counts)
bias = self.__index_base - self.__index_start
old_positive_limit = size - bias
# 2 ** ceil(log2(needed)) finds the smallest power of two that is larger
# or equal than needed:
# 2 ** ceil(log2(1)) == 1
# 2 ** ceil(log2(2)) == 2
# 2 ** ceil(log2(3)) == 4
# 2 ** ceil(log2(4)) == 4
# 2 ** ceil(log2(5)) == 8
# 2 ** ceil(log2(6)) == 8
# 2 ** ceil(log2(7)) == 8
# 2 ** ceil(log2(8)) == 8
new_size = min(2 ** ceil(log2(needed)), max_size)
new_positive_limit = new_size - bias
tmp = [0] * new_size
tmp[new_positive_limit:] = self._counts[old_positive_limit:]
tmp[0:old_positive_limit] = self._counts[0:old_positive_limit]
self._counts = tmp
@property
def offset(self) -> int:
return self.__index_start
def __len__(self) -> int:
if len(self._counts) == 0:
return 0
if self.__index_end == self.__index_start and self[0] == 0:
return 0
return self.__index_end - self.__index_start + 1
def __getitem__(self, key: int) -> int:
bias = self.__index_base - self.__index_start
if key < bias:
key += len(self._counts)
key -= bias
return self._counts[key]
def downscale(self, amount: int) -> None:
"""
Rotates, then collapses 2 ** amount to 1 buckets.
"""
bias = self.__index_base - self.__index_start
if bias != 0:
self.__index_base = self.__index_start
# [0, 1, 2, 3, 4] Original backing array
self._counts = self._counts[::-1]
# [4, 3, 2, 1, 0]
self._counts = self._counts[:bias][::-1] + self._counts[bias:][::-1]
# [3, 4, 0, 1, 2] This is a rotation of the backing array.
size = 1 + self.__index_end - self.__index_start
each = 1 << amount
inpos = 0
outpos = 0
pos = self.__index_start
while pos <= self.__index_end:
mod = pos % each
if mod < 0:
mod += each
index = mod
while index < each and inpos < size:
if outpos != inpos:
self._counts[outpos] += self._counts[inpos]
self._counts[inpos] = 0
inpos += 1
pos += 1
index += 1
outpos += 1
self.__index_start >>= amount
self.__index_end >>= amount
self.__index_base = self.__index_start
def increment_bucket(self, bucket_index: int, increment: int = 1) -> None:
self._counts[bucket_index] += increment
@@ -0,0 +1,96 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from abc import ABC, abstractmethod
class Mapping(ABC):
"""
Parent class for `LogarithmMapping` and `ExponentialMapping`.
"""
# pylint: disable=no-member
def __new__(cls, scale: int):
with cls._mappings_lock:
# cls._mappings and cls._mappings_lock are implemented in each of
# the child classes as a dictionary and a lock, respectively. They
# are not instantiated here because that would lead to both child
# classes having the same instance of cls._mappings and
# cls._mappings_lock.
if scale not in cls._mappings:
cls._mappings[scale] = super().__new__(cls)
cls._mappings[scale]._init(scale)
return cls._mappings[scale]
@abstractmethod
def _init(self, scale: int) -> None:
# pylint: disable=attribute-defined-outside-init
if scale > self._get_max_scale():
raise Exception(f"scale is larger than {self._max_scale}")
if scale < self._get_min_scale():
raise Exception(f"scale is smaller than {self._min_scale}")
# The size of the exponential histogram buckets is determined by a
# parameter known as scale, larger values of scale will produce smaller
# buckets. Bucket boundaries of the exponential histogram are located
# at integer powers of the base, where:
#
# base = 2 ** (2 ** (-scale))
# https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md#all-scales-use-the-logarithm-function
self._scale = scale
@abstractmethod
def _get_min_scale(self) -> int:
"""
Return the smallest possible value for the mapping scale
"""
@abstractmethod
def _get_max_scale(self) -> int:
"""
Return the largest possible value for the mapping scale
"""
@abstractmethod
def map_to_index(self, value: float) -> int:
"""
Maps positive floating point values to indexes corresponding to
`Mapping.scale`. Implementations are not expected to handle zeros,
+inf, NaN, or negative values.
"""
@abstractmethod
def get_lower_boundary(self, index: int) -> float:
"""
Returns the lower boundary of a given bucket index. The index is
expected to map onto a range that is at least partially inside the
range of normal floating point values. If the corresponding
bucket's upper boundary is less than or equal to 2 ** -1022,
:class:`~opentelemetry.sdk.metrics.MappingUnderflowError`
will be raised. If the corresponding bucket's lower boundary is greater
than ``sys.float_info.max``,
:class:`~opentelemetry.sdk.metrics.MappingOverflowError`
will be raised.
"""
@property
def scale(self) -> int:
"""
Returns the parameter that controls the resolution of this mapping.
See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/datamodel.md#exponential-scale
"""
return self._scale
@@ -0,0 +1,26 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
class MappingUnderflowError(Exception):
"""
Raised when computing the lower boundary of an index that maps into a
denormal floating point value.
"""
class MappingOverflowError(Exception):
"""
Raised when computing the lower boundary of an index that maps into +inf.
"""
@@ -0,0 +1,139 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from math import ldexp
from threading import Lock
from mysql.opentelemetry.sdk.metrics._internal.exponential_histogram.mapping import (
Mapping,
)
from mysql.opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.errors import (
MappingOverflowError,
MappingUnderflowError,
)
from mysql.opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.ieee_754 import (
MANTISSA_WIDTH,
MAX_NORMAL_EXPONENT,
MIN_NORMAL_EXPONENT,
MIN_NORMAL_VALUE,
get_ieee_754_exponent,
get_ieee_754_mantissa,
)
class ExponentMapping(Mapping):
# Reference implementation here:
# https://github.com/open-telemetry/opentelemetry-go/blob/0e6f9c29c10d6078e8131418e1d1d166c7195d61/sdk/metric/aggregator/exponential/mapping/exponent/exponent.go
_mappings = {}
_mappings_lock = Lock()
_min_scale = -10
_max_scale = 0
def _get_min_scale(self):
# _min_scale defines the point at which the exponential mapping
# function becomes useless for 64-bit floats. With scale -10, ignoring
# subnormal values, bucket indices range from -1 to 1.
return -10
def _get_max_scale(self):
# _max_scale is the largest scale supported by exponential mapping. Use
# a logarithm mapping for larger scales.
return 0
def _init(self, scale: int):
# pylint: disable=attribute-defined-outside-init
super()._init(scale)
# self._min_normal_lower_boundary_index is the largest index such that
# base ** index < MIN_NORMAL_VALUE and
# base ** (index + 1) >= MIN_NORMAL_VALUE. An exponential histogram
# bucket with this index covers the range
# (base ** index, base (index + 1)], including MIN_NORMAL_VALUE. This
# is the smallest valid index that contains at least one normal value.
index = MIN_NORMAL_EXPONENT >> -self._scale
if -self._scale < 2:
# For scales -1 and 0, the maximum value 2 ** -1022 is a
# power-of-two multiple, meaning base ** index == MIN_NORMAL_VALUE.
# Subtracting 1 so that base ** (index + 1) == MIN_NORMAL_VALUE.
index -= 1
self._min_normal_lower_boundary_index = index
# self._max_normal_lower_boundary_index is the index such that
# base**index equals the greatest representable lower boundary. An
# exponential histogram bucket with this index covers the range
# ((2 ** 1024) / base, 2 ** 1024], which includes mysql.opentelemetry.sdk.
# metrics._internal.exponential_histogram.ieee_754.MAX_NORMAL_VALUE.
# This bucket is incomplete, since the upper boundary cannot be
# represented. One greater than this index corresponds with the bucket
# containing values > 2 ** 1024.
self._max_normal_lower_boundary_index = MAX_NORMAL_EXPONENT >> -self._scale
def map_to_index(self, value: float) -> int:
if value < MIN_NORMAL_VALUE:
return self._min_normal_lower_boundary_index
exponent = get_ieee_754_exponent(value)
# Positive integers are represented in binary as having an infinite
# amount of leading zeroes, for example 2 is represented as ...00010.
# A negative integer -x is represented in binary as the complement of
# (x - 1). For example, -4 is represented as the complement of 4 - 1
# == 3. 3 is represented as ...00011. Its compliment is ...11100, the
# binary representation of -4.
# get_ieee_754_mantissa(value) gets the positive integer made up
# from the rightmost MANTISSA_WIDTH bits (the mantissa) of the IEEE
# 754 representation of value. If value is an exact power of 2, all
# these MANTISSA_WIDTH bits would be all zeroes, and when 1 is
# subtracted the resulting value is -1. The binary representation of
# -1 is ...111, so when these bits are right shifted MANTISSA_WIDTH
# places, the resulting value for correction is -1. If value is not an
# exact power of 2, at least one of the rightmost MANTISSA_WIDTH
# bits would be 1 (even for values whose decimal part is 0, like 5.0
# since the IEEE 754 of such number is too the product of a power of 2
# (defined in the exponent part of the IEEE 754 representation) and the
# value defined in the mantissa). Having at least one of the rightmost
# MANTISSA_WIDTH bit being 1 means that get_ieee_754(value) will
# always be greater or equal to 1, and when 1 is subtracted, the
# result will be greater or equal to 0, whose representation in binary
# will be of at most MANTISSA_WIDTH ones that have an infinite
# amount of leading zeroes. When those MANTISSA_WIDTH bits are
# shifted to the right MANTISSA_WIDTH places, the resulting value
# will be 0.
# In summary, correction will be -1 if value is a power of 2, 0 if not.
# FIXME Document why we can assume value will not be 0, inf, or NaN.
correction = (get_ieee_754_mantissa(value) - 1) >> MANTISSA_WIDTH
return (exponent + correction) >> -self._scale
def get_lower_boundary(self, index: int) -> float:
if index < self._min_normal_lower_boundary_index:
raise MappingUnderflowError()
if index > self._max_normal_lower_boundary_index:
raise MappingOverflowError()
return ldexp(1, index << -self._scale)
@property
def scale(self) -> int:
return self._scale
@@ -0,0 +1,118 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from ctypes import c_double, c_uint64
from sys import float_info
# IEEE 754 64-bit floating point numbers use 11 bits for the exponent and 52
# bits for the mantissa.
MANTISSA_WIDTH = 52
EXPONENT_WIDTH = 11
# This mask is equivalent to 52 "1" bits (there are 13 hexadecimal 4-bit "f"s
# in the mantissa mask, 13 * 4 == 52) or 0xfffffffffffff in hexadecimal.
MANTISSA_MASK = (1 << MANTISSA_WIDTH) - 1
# There are 11 bits for the exponent, but the exponent values 0 (11 "0"
# bits) and 2047 (11 "1" bits) have special meanings so the exponent range is
# from 1 to 2046. To calculate the exponent value, 1023 (the bias) is
# subtracted from the exponent, so the exponent value range is from -1022 to
# +1023.
EXPONENT_BIAS = (2 ** (EXPONENT_WIDTH - 1)) - 1
# All the exponent mask bits are set to 1 for the 11 exponent bits.
EXPONENT_MASK = ((1 << EXPONENT_WIDTH) - 1) << MANTISSA_WIDTH
# The sign mask has the first bit set to 1 and the rest to 0.
SIGN_MASK = 1 << (EXPONENT_WIDTH + MANTISSA_WIDTH)
# For normal floating point numbers, the exponent can have a value in the
# range [-1022, 1023].
MIN_NORMAL_EXPONENT = -EXPONENT_BIAS + 1
MAX_NORMAL_EXPONENT = EXPONENT_BIAS
# The smallest possible normal value is 2.2250738585072014e-308.
# This value is the result of using the smallest possible number in the
# mantissa, 1.0000000000000000000000000000000000000000000000000000 (52 "0"s in
# the fractional part) and a single "1" in the exponent.
# Finally 1 * (2 ** -1022) = 2.2250738585072014e-308.
MIN_NORMAL_VALUE = float_info.min
# Greatest possible normal value (1.7976931348623157e+308)
# The binary representation of a float in scientific notation uses (for the
# mantissa) one bit for the integer part (which is implicit) and 52 bits for
# the fractional part. Consider a float binary 1.111. It is equal to 1 + 1/2 +
# 1/4 + 1/8. The greatest possible value in the 52-bit binary mantissa would be
# then 1.1111111111111111111111111111111111111111111111111111 (52 "1"s in the
# fractional part) whose decimal value is 1.9999999999999998. Finally,
# 1.9999999999999998 * (2 ** 1023) = 1.7976931348623157e+308.
MAX_NORMAL_VALUE = float_info.max
def get_ieee_754_exponent(value: float) -> int:
"""
Gets the exponent of the IEEE 754 representation of a float.
"""
return (
(
# This step gives the integer that corresponds to the IEEE 754
# representation of a float. For example, consider
# -MAX_NORMAL_VALUE for an example. We choose this value because
# of its binary representation which makes easy to understand the
# subsequent operations.
#
# c_uint64.from_buffer(c_double(-MAX_NORMAL_VALUE)).value == 18442240474082181119
# bin(18442240474082181119) == '0b1111111111101111111111111111111111111111111111111111111111111111'
#
# The first bit of the previous binary number is the sign bit: 1 (1 means negative, 0 means positive)
# The next 11 bits are the exponent bits: 11111111110
# The next 52 bits are the mantissa bits: 1111111111111111111111111111111111111111111111111111
#
# This step isolates the exponent bits, turning every bit outside
# of the exponent field (sign and mantissa bits) to 0.
c_uint64.from_buffer(c_double(value)).value
& EXPONENT_MASK
# For the example this means:
# 18442240474082181119 & EXPONENT_MASK == 9214364837600034816
# bin(9214364837600034816) == '0b111111111100000000000000000000000000000000000000000000000000000'
# Notice that the previous binary representation does not include
# leading zeroes, so the sign bit is not included since it is a
# zero.
)
# This step moves the exponent bits to the right, removing the
# mantissa bits that were set to 0 by the previous step. This
# leaves the IEEE 754 exponent value, ready for the next step.
>> MANTISSA_WIDTH
# For the example this means:
# 9214364837600034816 >> MANTISSA_WIDTH == 2046
# bin(2046) == '0b11111111110'
# As shown above, these are the original 11 bits that correspond to the
# exponent.
# This step subtracts the exponent bias from the IEEE 754 value,
# leaving the actual exponent value.
) - EXPONENT_BIAS
# For the example this means:
# 2046 - EXPONENT_BIAS == 1023
# As mentioned in a comment above, the largest value for the exponent is
def get_ieee_754_mantissa(value: float) -> int:
return (
c_uint64.from_buffer(c_double(value)).value
# This step isolates the mantissa bits. There is no need to do any
# bit shifting as the mantissa bits are already the rightmost field
# in an IEEE 754 representation.
& MANTISSA_MASK
)
@@ -0,0 +1,132 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from math import exp, floor, ldexp, log
from threading import Lock
from mysql.opentelemetry.sdk.metrics._internal.exponential_histogram.mapping import (
Mapping,
)
from mysql.opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.errors import (
MappingOverflowError,
MappingUnderflowError,
)
from mysql.opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.ieee_754 import (
MAX_NORMAL_EXPONENT,
MIN_NORMAL_EXPONENT,
MIN_NORMAL_VALUE,
get_ieee_754_exponent,
get_ieee_754_mantissa,
)
class LogarithmMapping(Mapping):
# Reference implementation here:
# https://github.com/open-telemetry/opentelemetry-go/blob/0e6f9c29c10d6078e8131418e1d1d166c7195d61/sdk/metric/aggregator/exponential/mapping/logarithm/logarithm.go
_mappings = {}
_mappings_lock = Lock()
_min_scale = 1
_max_scale = 20
def _get_min_scale(self):
# _min_scale ensures that ExponentMapping is used for zero and negative
# scale values.
return self._min_scale
def _get_max_scale(self):
# FIXME The Go implementation uses a value of 20 here, find out the
# right value for this implementation, more information here:
# https://github.com/lightstep/otel-launcher-go/blob/c9ca8483be067a39ab306b09060446e7fda65f35/lightstep/sdk/metric/aggregator/histogram/structure/README.md#mapping-function
# https://github.com/open-telemetry/opentelemetry-go/blob/0e6f9c29c10d6078e8131418e1d1d166c7195d61/sdk/metric/aggregator/exponential/mapping/logarithm/logarithm.go#L32-L45
return self._max_scale
def _init(self, scale: int):
# pylint: disable=attribute-defined-outside-init
super()._init(scale)
# self._scale_factor is defined as a multiplier because multiplication
# is faster than division. self._scale_factor is defined as:
# index = log(value) * self._scale_factor
# Where:
# index = log(value) / log(base)
# index = log(value) / log(2 ** (2 ** -scale))
# index = log(value) / ((2 ** -scale) * log(2))
# index = log(value) * ((1 / log(2)) * (2 ** scale))
# self._scale_factor = ((1 / log(2)) * (2 ** scale))
# self._scale_factor = (1 /log(2)) * (2 ** scale)
# self._scale_factor = ldexp(1 / log(2), scale)
# This implementation was copied from a Java prototype. See:
# https://github.com/newrelic-experimental/newrelic-sketch-java/blob/1ce245713603d61ba3a4510f6df930a5479cd3f6/src/main/java/com/newrelic/nrsketch/indexer/LogIndexer.java
# for the equations used here.
self._scale_factor = ldexp(1 / log(2), scale)
# self._min_normal_lower_boundary_index is the index such that
# base ** index == MIN_NORMAL_VALUE. An exponential histogram bucket
# with this index covers the range
# (MIN_NORMAL_VALUE, MIN_NORMAL_VALUE * base]. One less than this index
# corresponds with the bucket containing values <= MIN_NORMAL_VALUE.
self._min_normal_lower_boundary_index = MIN_NORMAL_EXPONENT << self._scale
# self._max_normal_lower_boundary_index is the index such that
# base ** index equals the greatest representable lower boundary. An
# exponential histogram bucket with this index covers the range
# ((2 ** 1024) / base, 2 ** 1024], which includes mysql.opentelemetry.sdk.
# metrics._internal.exponential_histogram.ieee_754.MAX_NORMAL_VALUE.
# This bucket is incomplete, since the upper boundary cannot be
# represented. One greater than this index corresponds with the bucket
# containing values > 2 ** 1024.
self._max_normal_lower_boundary_index = (
(MAX_NORMAL_EXPONENT + 1) << self._scale
) - 1
def map_to_index(self, value: float) -> int:
"""
Maps positive floating point values to indexes corresponding to scale.
"""
# value is subnormal
if value <= MIN_NORMAL_VALUE:
return self._min_normal_lower_boundary_index - 1
# value is an exact power of two.
if get_ieee_754_mantissa(value) == 0:
exponent = get_ieee_754_exponent(value)
return (exponent << self._scale) - 1
return min(
floor(log(value) * self._scale_factor),
self._max_normal_lower_boundary_index,
)
def get_lower_boundary(self, index: int) -> float:
if index >= self._max_normal_lower_boundary_index:
if index == self._max_normal_lower_boundary_index:
return 2 * exp((index - (1 << self._scale)) / self._scale_factor)
raise MappingOverflowError()
if index <= self._min_normal_lower_boundary_index:
if index == self._min_normal_lower_boundary_index:
return MIN_NORMAL_VALUE
if index == self._min_normal_lower_boundary_index - 1:
return exp((index + (1 << self._scale)) / self._scale_factor) / 2
raise MappingUnderflowError()
return exp(index / self._scale_factor)
@property
def scale(self) -> int:
return self._scale
@@ -0,0 +1,525 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import math
import os
from abc import ABC, abstractmethod
from enum import Enum
from logging import getLogger
from os import environ, linesep
from sys import stdout
from threading import Event, Lock, RLock, Thread
from time import time_ns
from typing import IO, Callable, Dict, Iterable, Optional
# This kind of import is needed to avoid Sphinx errors.
import mysql.opentelemetry.sdk.metrics._internal
from mysql.opentelemetry.context import (
_SUPPRESS_INSTRUMENTATION_KEY,
attach,
detach,
set_value,
)
from mysql.opentelemetry.sdk.environment_variables import (
OTEL_METRIC_EXPORT_INTERVAL,
OTEL_METRIC_EXPORT_TIMEOUT,
)
from mysql.opentelemetry.sdk.metrics._internal.aggregation import (
AggregationTemporality,
DefaultAggregation,
)
from mysql.opentelemetry.sdk.metrics._internal.exceptions import MetricsTimeoutError
from mysql.opentelemetry.sdk.metrics._internal.instrument import (
Counter,
Histogram,
ObservableCounter,
ObservableGauge,
ObservableUpDownCounter,
UpDownCounter,
_Counter,
_Histogram,
_ObservableCounter,
_ObservableGauge,
_ObservableUpDownCounter,
_UpDownCounter,
)
from mysql.opentelemetry.sdk.metrics._internal.point import MetricsData
from mysql.opentelemetry.util._once import Once
from typing_extensions import final
_logger = getLogger(__name__)
class MetricExportResult(Enum):
"""Result of exporting a metric
Can be any of the following values:"""
SUCCESS = 0
FAILURE = 1
class MetricExporter(ABC):
"""Interface for exporting metrics.
Interface to be implemented by services that want to export metrics received
in their own format.
Args:
preferred_temporality: Used by `opentelemetry.sdk.metrics.export.PeriodicExportingMetricReader` to
configure exporter level preferred temporality. See `opentelemetry.sdk.metrics.export.MetricReader` for
more details on what preferred temporality is.
preferred_aggregation: Used by `opentelemetry.sdk.metrics.export.PeriodicExportingMetricReader` to
configure exporter level preferred aggregation. See `opentelemetry.sdk.metrics.export.MetricReader` for
more details on what preferred aggregation is.
"""
def __init__(
self,
preferred_temporality: Dict[type, AggregationTemporality] = None,
preferred_aggregation: Dict[
type, "mysql.opentelemetry.sdk.metrics.view.Aggregation"
] = None,
) -> None:
self._preferred_temporality = preferred_temporality
self._preferred_aggregation = preferred_aggregation
@abstractmethod
def export(
self,
metrics_data: MetricsData,
timeout_millis: float = 10_000,
**kwargs,
) -> MetricExportResult:
"""Exports a batch of telemetry data.
Args:
metrics: The list of `opentelemetry.sdk.metrics.export.Metric` objects to be exported
Returns:
The result of the export
"""
@abstractmethod
def force_flush(self, timeout_millis: float = 10_000) -> bool:
"""
Ensure that export of any metrics currently received by the exporter
are completed as soon as possible.
"""
@abstractmethod
def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None:
"""Shuts down the exporter.
Called when the SDK is shut down.
"""
class ConsoleMetricExporter(MetricExporter):
"""Implementation of :class:`MetricExporter` that prints metrics to the
console.
This class can be used for diagnostic purposes. It prints the exported
metrics to the console STDOUT.
"""
def __init__(
self,
out: IO = stdout,
formatter: Callable[
["mysql.opentelemetry.sdk.metrics.export.MetricsData"], str
] = lambda metrics_data: metrics_data.to_json()
+ linesep,
preferred_temporality: Dict[type, AggregationTemporality] = None,
preferred_aggregation: Dict[
type, "mysql.opentelemetry.sdk.metrics.view.Aggregation"
] = None,
):
super().__init__(
preferred_temporality=preferred_temporality,
preferred_aggregation=preferred_aggregation,
)
self.out = out
self.formatter = formatter
def export(
self,
metrics_data: MetricsData,
timeout_millis: float = 10_000,
**kwargs,
) -> MetricExportResult:
self.out.write(self.formatter(metrics_data))
self.out.flush()
return MetricExportResult.SUCCESS
def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None:
pass
def force_flush(self, timeout_millis: float = 10_000) -> bool:
return True
class MetricReader(ABC):
# pylint: disable=too-many-branches
"""
Base class for all metric readers
Args:
preferred_temporality: A mapping between instrument classes and
aggregation temporality. By default uses CUMULATIVE for all instrument
classes. This mapping will be used to define the default aggregation
temporality of every instrument class. If the user wants to make a
change in the default aggregation temporality of an instrument class,
it is enough to pass here a dictionary whose keys are the instrument
classes and the values are the corresponding desired aggregation
temporalities of the classes that the user wants to change, not all of
them. The classes not included in the passed dictionary will retain
their association to their default aggregation temporalities.
preferred_aggregation: A mapping between instrument classes and
aggregation instances. By default maps all instrument classes to an
instance of `DefaultAggregation`. This mapping will be used to
define the default aggregation of every instrument class. If the
user wants to make a change in the default aggregation of an
instrument class, it is enough to pass here a dictionary whose keys
are the instrument classes and the values are the corresponding
desired aggregation for the instrument classes that the user wants
to change, not necessarily all of them. The classes not included in
the passed dictionary will retain their association to their
default aggregations. The aggregation defined here will be
overridden by an aggregation defined by a view that is not
`DefaultAggregation`.
.. document protected _receive_metrics which is a intended to be overridden by subclass
.. automethod:: _receive_metrics
"""
def __init__(
self,
preferred_temporality: Dict[type, AggregationTemporality] = None,
preferred_aggregation: Dict[
type, "mysql.opentelemetry.sdk.metrics.view.Aggregation"
] = None,
) -> None:
self._collect: Callable[
[
"mysql.opentelemetry.sdk.metrics.export.MetricReader",
AggregationTemporality,
],
Iterable["mysql.opentelemetry.sdk.metrics.export.Metric"],
] = None
self._instrument_class_temporality = {
_Counter: AggregationTemporality.CUMULATIVE,
_UpDownCounter: AggregationTemporality.CUMULATIVE,
_Histogram: AggregationTemporality.CUMULATIVE,
_ObservableCounter: AggregationTemporality.CUMULATIVE,
_ObservableUpDownCounter: AggregationTemporality.CUMULATIVE,
_ObservableGauge: AggregationTemporality.CUMULATIVE,
}
if preferred_temporality is not None:
for temporality in preferred_temporality.values():
if temporality not in (
AggregationTemporality.CUMULATIVE,
AggregationTemporality.DELTA,
):
raise Exception(f"Invalid temporality value found {temporality}")
if preferred_temporality is not None:
for typ, temporality in preferred_temporality.items():
if typ is Counter:
self._instrument_class_temporality[_Counter] = temporality
elif typ is UpDownCounter:
self._instrument_class_temporality[_UpDownCounter] = temporality
elif typ is Histogram:
self._instrument_class_temporality[_Histogram] = temporality
elif typ is ObservableCounter:
self._instrument_class_temporality[_ObservableCounter] = temporality
elif typ is ObservableUpDownCounter:
self._instrument_class_temporality[
_ObservableUpDownCounter
] = temporality
elif typ is ObservableGauge:
self._instrument_class_temporality[_ObservableGauge] = temporality
else:
raise Exception(f"Invalid instrument class found {typ}")
self._preferred_temporality = preferred_temporality
self._instrument_class_aggregation = {
_Counter: DefaultAggregation(),
_UpDownCounter: DefaultAggregation(),
_Histogram: DefaultAggregation(),
_ObservableCounter: DefaultAggregation(),
_ObservableUpDownCounter: DefaultAggregation(),
_ObservableGauge: DefaultAggregation(),
}
if preferred_aggregation is not None:
for typ, aggregation in preferred_aggregation.items():
if typ is Counter:
self._instrument_class_aggregation[_Counter] = aggregation
elif typ is UpDownCounter:
self._instrument_class_aggregation[_UpDownCounter] = aggregation
elif typ is Histogram:
self._instrument_class_aggregation[_Histogram] = aggregation
elif typ is ObservableCounter:
self._instrument_class_aggregation[_ObservableCounter] = aggregation
elif typ is ObservableUpDownCounter:
self._instrument_class_aggregation[
_ObservableUpDownCounter
] = aggregation
elif typ is ObservableGauge:
self._instrument_class_aggregation[_ObservableGauge] = aggregation
else:
raise Exception(f"Invalid instrument class found {typ}")
@final
def collect(self, timeout_millis: float = 10_000) -> None:
"""Collects the metrics from the internal SDK state and
invokes the `_receive_metrics` with the collection.
Args:
timeout_millis: Amount of time in milliseconds before this function
raises a timeout error.
If any of the underlying ``collect`` methods called by this method
fails by any reason (including timeout) an exception will be raised
detailing the individual errors that caused this function to fail.
"""
if self._collect is None:
_logger.warning(
"Cannot call collect on a MetricReader until it is registered on a MeterProvider"
)
return
self._receive_metrics(
self._collect(self, timeout_millis=timeout_millis),
timeout_millis=timeout_millis,
)
@final
def _set_collect_callback(
self,
func: Callable[
[
"mysql.opentelemetry.sdk.metrics.export.MetricReader",
AggregationTemporality,
],
Iterable["mysql.opentelemetry.sdk.metrics.export.Metric"],
],
) -> None:
"""This function is internal to the SDK. It should not be called or overridden by users"""
self._collect = func
@abstractmethod
def _receive_metrics(
self,
metrics_data: "mysql.opentelemetry.sdk.metrics.export.MetricsData",
timeout_millis: float = 10_000,
**kwargs,
) -> None:
"""Called by `MetricReader.collect` when it receives a batch of metrics"""
def force_flush(self, timeout_millis: float = 10_000) -> bool:
self.collect(timeout_millis=timeout_millis)
return True
@abstractmethod
def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None:
"""Shuts down the MetricReader. This method provides a way
for the MetricReader to do any cleanup required. A metric reader can
only be shutdown once, any subsequent calls are ignored and return
failure status.
When a `MetricReader` is registered on a
:class:`~opentelemetry.sdk.metrics.MeterProvider`,
:meth:`~opentelemetry.sdk.metrics.MeterProvider.shutdown` will invoke this
automatically.
"""
class InMemoryMetricReader(MetricReader):
"""Implementation of `MetricReader` that returns its metrics from :func:`get_metrics_data`.
This is useful for e.g. unit tests.
"""
def __init__(
self,
preferred_temporality: Dict[type, AggregationTemporality] = None,
preferred_aggregation: Dict[
type, "mysql.opentelemetry.sdk.metrics.view.Aggregation"
] = None,
) -> None:
super().__init__(
preferred_temporality=preferred_temporality,
preferred_aggregation=preferred_aggregation,
)
self._lock = RLock()
self._metrics_data: (
"mysql.opentelemetry.sdk.metrics.export.MetricsData"
) = None
def get_metrics_data(
self,
) -> "mysql.opentelemetry.sdk.metrics.export.MetricsData":
"""Reads and returns current metrics from the SDK"""
with self._lock:
self.collect()
metrics_data = self._metrics_data
self._metrics_data = None
return metrics_data
def _receive_metrics(
self,
metrics_data: "mysql.opentelemetry.sdk.metrics.export.MetricsData",
timeout_millis: float = 10_000,
**kwargs,
) -> None:
with self._lock:
self._metrics_data = metrics_data
def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None:
pass
class PeriodicExportingMetricReader(MetricReader):
"""`PeriodicExportingMetricReader` is an implementation of `MetricReader`
that collects metrics based on a user-configurable time interval, and passes the
metrics to the configured exporter. If the time interval is set to `math.inf`, the
reader will not invoke periodic collection.
The configured exporter's :py:meth:`~MetricExporter.export` method will not be called
concurrently.
"""
def __init__(
self,
exporter: MetricExporter,
export_interval_millis: Optional[float] = None,
export_timeout_millis: Optional[float] = None,
) -> None:
# PeriodicExportingMetricReader defers to exporter for configuration
super().__init__(
preferred_temporality=exporter._preferred_temporality,
preferred_aggregation=exporter._preferred_aggregation,
)
# This lock is held whenever calling self._exporter.export() to prevent concurrent
# execution of MetricExporter.export()
# https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#exportbatch
self._export_lock = Lock()
self._exporter = exporter
if export_interval_millis is None:
try:
export_interval_millis = float(
environ.get(OTEL_METRIC_EXPORT_INTERVAL, 60000)
)
except ValueError:
_logger.warning(
"Found invalid value for export interval, using default"
)
export_interval_millis = 60000
if export_timeout_millis is None:
try:
export_timeout_millis = float(
environ.get(OTEL_METRIC_EXPORT_TIMEOUT, 30000)
)
except ValueError:
_logger.warning("Found invalid value for export timeout, using default")
export_timeout_millis = 30000
self._export_interval_millis = export_interval_millis
self._export_timeout_millis = export_timeout_millis
self._shutdown = False
self._shutdown_event = Event()
self._shutdown_once = Once()
self._daemon_thread = None
if self._export_interval_millis > 0 and self._export_interval_millis < math.inf:
self._daemon_thread = Thread(
name="OtelPeriodicExportingMetricReader",
target=self._ticker,
daemon=True,
)
self._daemon_thread.start()
if hasattr(os, "register_at_fork"):
os.register_at_fork(
after_in_child=self._at_fork_reinit
) # pylint: disable=protected-access
elif self._export_interval_millis <= 0:
raise ValueError(
f"interval value {self._export_interval_millis} is invalid \
and needs to be larger than zero and lower than infinity."
)
def _at_fork_reinit(self):
self._daemon_thread = Thread(
name="OtelPeriodicExportingMetricReader",
target=self._ticker,
daemon=True,
)
self._daemon_thread.start()
def _ticker(self) -> None:
interval_secs = self._export_interval_millis / 1e3
while not self._shutdown_event.wait(interval_secs):
try:
self.collect(timeout_millis=self._export_timeout_millis)
except MetricsTimeoutError:
_logger.warning(
"Metric collection timed out. Will try again after %s seconds",
interval_secs,
exc_info=True,
)
# one last collection below before shutting down completely
self.collect(timeout_millis=self._export_interval_millis)
def _receive_metrics(
self,
metrics_data: MetricsData,
timeout_millis: float = 10_000,
**kwargs,
) -> None:
if metrics_data is None:
return
token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True))
try:
with self._export_lock:
self._exporter.export(metrics_data, timeout_millis=timeout_millis)
except Exception as e: # pylint: disable=broad-except,invalid-name
_logger.exception("Exception while exporting metrics %s", str(e))
detach(token)
def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None:
deadline_ns = time_ns() + timeout_millis * 10**6
def _shutdown():
self._shutdown = True
did_set = self._shutdown_once.do_once(_shutdown)
if not did_set:
_logger.warning("Can't shutdown multiple times")
return
self._shutdown_event.set()
if self._daemon_thread:
self._daemon_thread.join(timeout=(deadline_ns - time_ns()) / 10**9)
self._exporter.shutdown(timeout=(deadline_ns - time_ns()) / 10**6)
def force_flush(self, timeout_millis: float = 10_000) -> bool:
super().force_flush(timeout_millis=timeout_millis)
self._exporter.force_flush(timeout_millis=timeout_millis)
return True
@@ -0,0 +1,224 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# pylint: disable=too-many-ancestors, unused-import
from logging import getLogger
from typing import Dict, Generator, Iterable, List, Optional, Union
# This kind of import is needed to avoid Sphinx errors.
import mysql.opentelemetry.sdk.metrics
from mysql.opentelemetry.metrics import (
CallbackT,
Counter as APICounter,
Histogram as APIHistogram,
ObservableCounter as APIObservableCounter,
ObservableGauge as APIObservableGauge,
ObservableUpDownCounter as APIObservableUpDownCounter,
UpDownCounter as APIUpDownCounter,
)
from mysql.opentelemetry.metrics._internal.instrument import CallbackOptions
from mysql.opentelemetry.sdk.metrics._internal.measurement import Measurement
from mysql.opentelemetry.sdk.util.instrumentation import InstrumentationScope
_logger = getLogger(__name__)
_ERROR_MESSAGE = "Expected ASCII string of maximum length 63 characters but got {}"
class _Synchronous:
def __init__(
self,
name: str,
instrumentation_scope: InstrumentationScope,
measurement_consumer: "mysql.opentelemetry.sdk.metrics.MeasurementConsumer",
unit: str = "",
description: str = "",
):
# pylint: disable=no-member
result = self._check_name_unit_description(name, unit, description)
if result["name"] is None:
raise Exception(_ERROR_MESSAGE.format(name))
if result["unit"] is None:
raise Exception(_ERROR_MESSAGE.format(unit))
name = result["name"]
unit = result["unit"]
description = result["description"]
self.name = name.lower()
self.unit = unit
self.description = description
self.instrumentation_scope = instrumentation_scope
self._measurement_consumer = measurement_consumer
super().__init__(name, unit=unit, description=description)
class _Asynchronous:
def __init__(
self,
name: str,
instrumentation_scope: InstrumentationScope,
measurement_consumer: "mysql.opentelemetry.sdk.metrics.MeasurementConsumer",
callbacks: Optional[Iterable[CallbackT]] = None,
unit: str = "",
description: str = "",
):
# pylint: disable=no-member
result = self._check_name_unit_description(name, unit, description)
if result["name"] is None:
raise Exception(_ERROR_MESSAGE.format(name))
if result["unit"] is None:
raise Exception(_ERROR_MESSAGE.format(unit))
name = result["name"]
unit = result["unit"]
description = result["description"]
self.name = name.lower()
self.unit = unit
self.description = description
self.instrumentation_scope = instrumentation_scope
self._measurement_consumer = measurement_consumer
super().__init__(name, callbacks, unit=unit, description=description)
self._callbacks: List[CallbackT] = []
if callbacks is not None:
for callback in callbacks:
if isinstance(callback, Generator):
# advance generator to it's first yield
next(callback)
def inner(
options: CallbackOptions,
callback=callback,
) -> Iterable[Measurement]:
try:
return callback.send(options)
except StopIteration:
return []
self._callbacks.append(inner)
else:
self._callbacks.append(callback)
def callback(self, callback_options: CallbackOptions) -> Iterable[Measurement]:
for callback in self._callbacks:
try:
for api_measurement in callback(callback_options):
yield Measurement(
api_measurement.value,
instrument=self,
attributes=api_measurement.attributes,
)
except Exception: # pylint: disable=broad-except
_logger.exception("Callback failed for instrument %s.", self.name)
class Counter(_Synchronous, APICounter):
def __new__(cls, *args, **kwargs):
if cls is Counter:
raise TypeError("Counter must be instantiated via a meter.")
return super().__new__(cls)
def add(self, amount: Union[int, float], attributes: Dict[str, str] = None):
if amount < 0:
_logger.warning("Add amount must be non-negative on Counter %s.", self.name)
return
self._measurement_consumer.consume_measurement(
Measurement(amount, self, attributes)
)
class UpDownCounter(_Synchronous, APIUpDownCounter):
def __new__(cls, *args, **kwargs):
if cls is UpDownCounter:
raise TypeError("UpDownCounter must be instantiated via a meter.")
return super().__new__(cls)
def add(self, amount: Union[int, float], attributes: Dict[str, str] = None):
self._measurement_consumer.consume_measurement(
Measurement(amount, self, attributes)
)
class ObservableCounter(_Asynchronous, APIObservableCounter):
def __new__(cls, *args, **kwargs):
if cls is ObservableCounter:
raise TypeError("ObservableCounter must be instantiated via a meter.")
return super().__new__(cls)
class ObservableUpDownCounter(_Asynchronous, APIObservableUpDownCounter):
def __new__(cls, *args, **kwargs):
if cls is ObservableUpDownCounter:
raise TypeError("ObservableUpDownCounter must be instantiated via a meter.")
return super().__new__(cls)
class Histogram(_Synchronous, APIHistogram):
def __new__(cls, *args, **kwargs):
if cls is Histogram:
raise TypeError("Histogram must be instantiated via a meter.")
return super().__new__(cls)
def record(self, amount: Union[int, float], attributes: Dict[str, str] = None):
if amount < 0:
_logger.warning(
"Record amount must be non-negative on Histogram %s.",
self.name,
)
return
self._measurement_consumer.consume_measurement(
Measurement(amount, self, attributes)
)
class ObservableGauge(_Asynchronous, APIObservableGauge):
def __new__(cls, *args, **kwargs):
if cls is ObservableGauge:
raise TypeError("ObservableGauge must be instantiated via a meter.")
return super().__new__(cls)
# Below classes exist to prevent the direct instantiation
class _Counter(Counter):
pass
class _UpDownCounter(UpDownCounter):
pass
class _ObservableCounter(ObservableCounter):
pass
class _ObservableUpDownCounter(ObservableUpDownCounter):
pass
class _Histogram(Histogram):
pass
class _ObservableGauge(ObservableGauge):
pass
@@ -0,0 +1,30 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from dataclasses import dataclass
from typing import Union
from mysql.opentelemetry.metrics import Instrument
from mysql.opentelemetry.util.types import Attributes
@dataclass(frozen=True)
class Measurement:
"""
Represents a data point reported via the metrics API to the SDK.
"""
value: Union[int, float]
instrument: Instrument
attributes: Attributes = None
@@ -0,0 +1,120 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# pylint: disable=unused-import
from abc import ABC, abstractmethod
from threading import Lock
from time import time_ns
from typing import Iterable, List, Mapping
# This kind of import is needed to avoid Sphinx errors.
import mysql.opentelemetry.sdk.metrics
import mysql.opentelemetry.sdk.metrics._internal.instrument
import mysql.opentelemetry.sdk.metrics._internal.sdk_configuration
from mysql.opentelemetry.metrics._internal.instrument import CallbackOptions
from mysql.opentelemetry.sdk.metrics._internal.exceptions import MetricsTimeoutError
from mysql.opentelemetry.sdk.metrics._internal.measurement import Measurement
from mysql.opentelemetry.sdk.metrics._internal.metric_reader_storage import (
MetricReaderStorage,
)
from mysql.opentelemetry.sdk.metrics._internal.point import Metric
class MeasurementConsumer(ABC):
@abstractmethod
def consume_measurement(self, measurement: Measurement) -> None:
pass
@abstractmethod
def register_asynchronous_instrument(
self,
instrument: (
"mysql.opentelemetry.sdk.metrics._internal.instrument_Asynchronous"
),
):
pass
@abstractmethod
def collect(
self,
metric_reader: "mysql.opentelemetry.sdk.metrics.MetricReader",
timeout_millis: float = 10_000,
) -> Iterable[Metric]:
pass
class SynchronousMeasurementConsumer(MeasurementConsumer):
def __init__(
self,
sdk_config: "mysql.opentelemetry.sdk.metrics._internal.SdkConfiguration",
) -> None:
self._lock = Lock()
self._sdk_config = sdk_config
# should never be mutated
self._reader_storages: Mapping[
"mysql.opentelemetry.sdk.metrics.MetricReader", MetricReaderStorage
] = {
reader: MetricReaderStorage(
sdk_config,
reader._instrument_class_temporality,
reader._instrument_class_aggregation,
)
for reader in sdk_config.metric_readers
}
self._async_instruments: List[
"mysql.opentelemetry.sdk.metrics._internal.instrument._Asynchronous"
] = []
def consume_measurement(self, measurement: Measurement) -> None:
for reader_storage in self._reader_storages.values():
reader_storage.consume_measurement(measurement)
def register_asynchronous_instrument(
self,
instrument: (
"mysql.opentelemetry.sdk.metrics._internal.instrument._Asynchronous"
),
) -> None:
with self._lock:
self._async_instruments.append(instrument)
def collect(
self,
metric_reader: "mysql.opentelemetry.sdk.metrics.MetricReader",
timeout_millis: float = 10_000,
) -> Iterable[Metric]:
with self._lock:
metric_reader_storage = self._reader_storages[metric_reader]
# for now, just use the defaults
callback_options = CallbackOptions()
deadline_ns = time_ns() + timeout_millis * 10**6
default_timeout_millis = 10000 * 10**6
for async_instrument in self._async_instruments:
remaining_time = deadline_ns - time_ns()
if remaining_time < default_timeout_millis:
callback_options = CallbackOptions(timeout_millis=remaining_time)
measurements = async_instrument.callback(callback_options)
if time_ns() >= deadline_ns:
raise MetricsTimeoutError("Timed out while executing callback")
for measurement in measurements:
metric_reader_storage.consume_measurement(measurement)
return self._reader_storages[metric_reader].collect()
@@ -0,0 +1,299 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from logging import getLogger
from threading import RLock
from time import time_ns
from typing import Dict, List
from mysql.opentelemetry.metrics import (
Asynchronous,
Counter,
Instrument,
ObservableCounter,
)
from mysql.opentelemetry.sdk.metrics._internal._view_instrument_match import (
_ViewInstrumentMatch,
)
from mysql.opentelemetry.sdk.metrics._internal.aggregation import (
Aggregation,
ExplicitBucketHistogramAggregation,
_DropAggregation,
_ExplicitBucketHistogramAggregation,
_ExponentialBucketHistogramAggregation,
_LastValueAggregation,
_SumAggregation,
)
from mysql.opentelemetry.sdk.metrics._internal.export import AggregationTemporality
from mysql.opentelemetry.sdk.metrics._internal.measurement import Measurement
from mysql.opentelemetry.sdk.metrics._internal.point import (
ExponentialHistogram,
Gauge,
Histogram,
Metric,
MetricsData,
ResourceMetrics,
ScopeMetrics,
Sum,
)
from mysql.opentelemetry.sdk.metrics._internal.sdk_configuration import SdkConfiguration
from mysql.opentelemetry.sdk.metrics._internal.view import View
from mysql.opentelemetry.sdk.util.instrumentation import InstrumentationScope
_logger = getLogger(__name__)
_DEFAULT_VIEW = View(instrument_name="")
class MetricReaderStorage:
"""The SDK's storage for a given reader"""
def __init__(
self,
sdk_config: SdkConfiguration,
instrument_class_temporality: Dict[type, AggregationTemporality],
instrument_class_aggregation: Dict[type, Aggregation],
) -> None:
self._lock = RLock()
self._sdk_config = sdk_config
self._instrument_view_instrument_matches: Dict[
Instrument, List[_ViewInstrumentMatch]
] = {}
self._instrument_class_temporality = instrument_class_temporality
self._instrument_class_aggregation = instrument_class_aggregation
def _get_or_init_view_instrument_match(
self, instrument: Instrument
) -> List[_ViewInstrumentMatch]:
# Optimistically get the relevant views for the given instrument. Once set for a given
# instrument, the mapping will never change
if instrument in self._instrument_view_instrument_matches:
return self._instrument_view_instrument_matches[instrument]
with self._lock:
# double check if it was set before we held the lock
if instrument in self._instrument_view_instrument_matches:
return self._instrument_view_instrument_matches[instrument]
# not present, hold the lock and add a new mapping
view_instrument_matches = []
self._handle_view_instrument_match(instrument, view_instrument_matches)
# if no view targeted the instrument, use the default
if not view_instrument_matches:
view_instrument_matches.append(
_ViewInstrumentMatch(
view=_DEFAULT_VIEW,
instrument=instrument,
instrument_class_aggregation=(
self._instrument_class_aggregation
),
)
)
self._instrument_view_instrument_matches[
instrument
] = view_instrument_matches
return view_instrument_matches
def consume_measurement(self, measurement: Measurement) -> None:
for view_instrument_match in self._get_or_init_view_instrument_match(
measurement.instrument
):
view_instrument_match.consume_measurement(measurement)
def collect(self) -> MetricsData:
# Use a list instead of yielding to prevent a slow reader from holding
# SDK locks
# While holding the lock, new _ViewInstrumentMatch can't be added from
# another thread (so we are sure we collect all existing view).
# However, instruments can still send measurements that will make it
# into the individual aggregations; collection will acquire those locks
# iteratively to keep locking as fine-grained as possible. One side
# effect is that end times can be slightly skewed among the metric
# streams produced by the SDK, but we still align the output timestamps
# for a single instrument.
collection_start_nanos = time_ns()
with self._lock:
instrumentation_scope_scope_metrics: (
Dict[InstrumentationScope, ScopeMetrics]
) = {}
for (
instrument,
view_instrument_matches,
) in self._instrument_view_instrument_matches.items():
aggregation_temporality = self._instrument_class_temporality[
instrument.__class__
]
metrics: List[Metric] = []
for view_instrument_match in view_instrument_matches:
if isinstance(
# pylint: disable=protected-access
view_instrument_match._aggregation,
_SumAggregation,
):
data = Sum(
aggregation_temporality=aggregation_temporality,
data_points=view_instrument_match.collect(
aggregation_temporality, collection_start_nanos
),
is_monotonic=isinstance(
instrument, (Counter, ObservableCounter)
),
)
elif isinstance(
# pylint: disable=protected-access
view_instrument_match._aggregation,
_LastValueAggregation,
):
data = Gauge(
data_points=view_instrument_match.collect(
aggregation_temporality, collection_start_nanos
)
)
elif isinstance(
# pylint: disable=protected-access
view_instrument_match._aggregation,
_ExplicitBucketHistogramAggregation,
):
data = Histogram(
data_points=view_instrument_match.collect(
aggregation_temporality, collection_start_nanos
),
aggregation_temporality=aggregation_temporality,
)
elif isinstance(
# pylint: disable=protected-access
view_instrument_match._aggregation,
_DropAggregation,
):
continue
elif isinstance(
# pylint: disable=protected-access
view_instrument_match._aggregation,
_ExponentialBucketHistogramAggregation,
):
data = ExponentialHistogram(
data_points=view_instrument_match.collect(
aggregation_temporality, collection_start_nanos
),
aggregation_temporality=aggregation_temporality,
)
metrics.append(
Metric(
# pylint: disable=protected-access
name=view_instrument_match._name,
description=view_instrument_match._description,
unit=view_instrument_match._instrument.unit,
data=data,
)
)
if instrument.instrumentation_scope not in (
instrumentation_scope_scope_metrics
):
instrumentation_scope_scope_metrics[
instrument.instrumentation_scope
] = ScopeMetrics(
scope=instrument.instrumentation_scope,
metrics=metrics,
schema_url=instrument.instrumentation_scope.schema_url,
)
else:
instrumentation_scope_scope_metrics[
instrument.instrumentation_scope
].metrics.extend(metrics)
return MetricsData(
resource_metrics=[
ResourceMetrics(
resource=self._sdk_config.resource,
scope_metrics=list(instrumentation_scope_scope_metrics.values()),
schema_url=self._sdk_config.resource.schema_url,
)
]
)
def _handle_view_instrument_match(
self,
instrument: Instrument,
view_instrument_matches: List["_ViewInstrumentMatch"],
) -> None:
for view in self._sdk_config.views:
# pylint: disable=protected-access
if not view._match(instrument):
continue
if not self._check_view_instrument_compatibility(view, instrument):
continue
new_view_instrument_match = _ViewInstrumentMatch(
view=view,
instrument=instrument,
instrument_class_aggregation=(self._instrument_class_aggregation),
)
for (
existing_view_instrument_matches
) in self._instrument_view_instrument_matches.values():
for existing_view_instrument_match in existing_view_instrument_matches:
if existing_view_instrument_match.conflicts(
new_view_instrument_match
):
_logger.warning(
"Views %s and %s will cause conflicting "
"metrics identities",
existing_view_instrument_match._view,
new_view_instrument_match._view,
)
view_instrument_matches.append(new_view_instrument_match)
@staticmethod
def _check_view_instrument_compatibility(
view: View, instrument: Instrument
) -> bool:
"""
Checks if a view and an instrument are compatible.
Returns `true` if they are compatible and a `_ViewInstrumentMatch`
object should be created, `false` otherwise.
"""
result = True
# pylint: disable=protected-access
if isinstance(instrument, Asynchronous) and isinstance(
view._aggregation, ExplicitBucketHistogramAggregation
):
_logger.warning(
"View %s and instrument %s will produce "
"semantic errors when matched, the view "
"has not been applied.",
view,
instrument,
)
result = False
return result
@@ -0,0 +1,258 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# pylint: disable=unused-import
from dataclasses import asdict, dataclass
from json import dumps, loads
from typing import Optional, Sequence, Union
# This kind of import is needed to avoid Sphinx errors.
import mysql.opentelemetry.sdk.metrics._internal
from mysql.opentelemetry.sdk.resources import Resource
from mysql.opentelemetry.sdk.util.instrumentation import InstrumentationScope
from mysql.opentelemetry.util.types import Attributes
@dataclass(frozen=True)
class NumberDataPoint:
"""Single data point in a timeseries that describes the time-varying scalar
value of a metric.
"""
attributes: Attributes
start_time_unix_nano: int
time_unix_nano: int
value: Union[int, float]
def to_json(self, indent=4) -> str:
return dumps(asdict(self), indent=indent)
@dataclass(frozen=True)
class HistogramDataPoint:
"""Single data point in a timeseries that describes the time-varying scalar
value of a metric.
"""
attributes: Attributes
start_time_unix_nano: int
time_unix_nano: int
count: int
sum: Union[int, float]
bucket_counts: Sequence[int]
explicit_bounds: Sequence[float]
min: float
max: float
def to_json(self, indent=4) -> str:
return dumps(asdict(self), indent=indent)
@dataclass(frozen=True)
class Buckets:
offset: int
bucket_counts: Sequence[int]
@dataclass(frozen=True)
class ExponentialHistogramDataPoint:
"""Single data point in a timeseries whose boundaries are defined by an
exponential function. This timeseries describes the time-varying scalar
value of a metric.
"""
attributes: Attributes
start_time_unix_nano: int
time_unix_nano: int
count: int
sum: Union[int, float]
scale: int
zero_count: int
positive: Buckets
negative: Buckets
flags: int
min: float
max: float
def to_json(self, indent=4) -> str:
return dumps(asdict(self), indent=indent)
@dataclass(frozen=True)
class ExponentialHistogram:
"""Represents the type of a metric that is calculated by aggregating as an
ExponentialHistogram of all reported measurements over a time interval.
"""
data_points: Sequence[ExponentialHistogramDataPoint]
aggregation_temporality: (
"mysql.opentelemetry.sdk.metrics.export.AggregationTemporality"
)
@dataclass(frozen=True)
class Sum:
"""Represents the type of a scalar metric that is calculated as a sum of
all reported measurements over a time interval."""
data_points: Sequence[NumberDataPoint]
aggregation_temporality: (
"mysql.opentelemetry.sdk.metrics.export.AggregationTemporality"
)
is_monotonic: bool
def to_json(self, indent=4) -> str:
return dumps(
{
"data_points": [
loads(data_point.to_json(indent=indent))
for data_point in self.data_points
],
"aggregation_temporality": self.aggregation_temporality,
"is_monotonic": self.is_monotonic,
},
indent=indent,
)
@dataclass(frozen=True)
class Gauge:
"""Represents the type of a scalar metric that always exports the current
value for every data point. It should be used for an unknown
aggregation."""
data_points: Sequence[NumberDataPoint]
def to_json(self, indent=4) -> str:
return dumps(
{
"data_points": [
loads(data_point.to_json(indent=indent))
for data_point in self.data_points
],
},
indent=indent,
)
@dataclass(frozen=True)
class Histogram:
"""Represents the type of a metric that is calculated by aggregating as a
histogram of all reported measurements over a time interval."""
data_points: Sequence[HistogramDataPoint]
aggregation_temporality: (
"mysql.opentelemetry.sdk.metrics.export.AggregationTemporality"
)
def to_json(self, indent=4) -> str:
return dumps(
{
"data_points": [
loads(data_point.to_json(indent=indent))
for data_point in self.data_points
],
"aggregation_temporality": self.aggregation_temporality,
},
indent=indent,
)
DataT = Union[Sum, Gauge, Histogram]
DataPointT = Union[NumberDataPoint, HistogramDataPoint]
@dataclass(frozen=True)
class Metric:
"""Represents a metric point in the OpenTelemetry data model to be
exported."""
name: str
description: Optional[str]
unit: Optional[str]
data: DataT
def to_json(self, indent=4) -> str:
return dumps(
{
"name": self.name,
"description": self.description or "",
"unit": self.unit or "",
"data": loads(self.data.to_json(indent=indent)),
},
indent=indent,
)
@dataclass(frozen=True)
class ScopeMetrics:
"""A collection of Metrics produced by a scope"""
scope: InstrumentationScope
metrics: Sequence[Metric]
schema_url: str
def to_json(self, indent=4) -> str:
return dumps(
{
"scope": loads(self.scope.to_json(indent=indent)),
"metrics": [
loads(metric.to_json(indent=indent)) for metric in self.metrics
],
"schema_url": self.schema_url,
},
indent=indent,
)
@dataclass(frozen=True)
class ResourceMetrics:
"""A collection of ScopeMetrics from a Resource"""
resource: Resource
scope_metrics: Sequence[ScopeMetrics]
schema_url: str
def to_json(self, indent=4) -> str:
return dumps(
{
"resource": loads(self.resource.to_json(indent=indent)),
"scope_metrics": [
loads(scope_metrics.to_json(indent=indent))
for scope_metrics in self.scope_metrics
],
"schema_url": self.schema_url,
},
indent=indent,
)
@dataclass(frozen=True)
class MetricsData:
"""An array of ResourceMetrics"""
resource_metrics: Sequence[ResourceMetrics]
def to_json(self, indent=4) -> str:
return dumps(
{
"resource_metrics": [
loads(resource_metrics.to_json(indent=indent))
for resource_metrics in self.resource_metrics
]
},
indent=indent,
)
@@ -0,0 +1,29 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# pylint: disable=unused-import
from dataclasses import dataclass
from typing import Sequence
# This kind of import is needed to avoid Sphinx errors.
import mysql.opentelemetry.sdk.metrics
import mysql.opentelemetry.sdk.resources
@dataclass
class SdkConfiguration:
resource: "mysql.opentelemetry.sdk.resources.Resource"
metric_readers: Sequence["mysql.opentelemetry.sdk.metrics.MetricReader"]
views: Sequence["mysql.opentelemetry.sdk.metrics.View"]
@@ -0,0 +1,155 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from fnmatch import fnmatch
from logging import getLogger
from typing import Optional, Set, Type
from mysql.opentelemetry.metrics import Instrument
from mysql.opentelemetry.sdk.metrics._internal.aggregation import (
Aggregation,
DefaultAggregation,
)
# FIXME import from typing when support for 3.7 is removed
from typing_extensions import final
_logger = getLogger(__name__)
class View:
"""
A `View` configuration parameters can be used for the following
purposes:
1. Match instruments: When an instrument matches a view, measurements
received by that instrument will be processed.
2. Customize metric streams: A metric stream is identified by a match
between a view and an instrument and a set of attributes. The metric
stream can be customized by certain attributes of the corresponding view.
The attributes documented next serve one of the previous two purposes.
Args:
instrument_type: This is an instrument matching attribute: the class the
instrument must be to match the view.
instrument_name: This is an instrument matching attribute: the name the
instrument must have to match the view. Wild card characters are supported. Wild
card characters should not be used with this attribute if the view has also a
``name`` defined.
meter_name: This is an instrument matching attribute: the name the
instrument meter must have to match the view.
meter_version: This is an instrument matching attribute: the version
the instrument meter must have to match the view.
meter_schema_url: This is an instrument matching attribute: the schema
URL the instrument meter must have to match the view.
name: This is a metric stream customizing attribute: the name of the
metric stream. If `None`, the name of the instrument will be used.
description: This is a metric stream customizing attribute: the
description of the metric stream. If `None`, the description of the instrument will
be used.
attribute_keys: This is a metric stream customizing attribute: this is
a set of attribute keys. If not `None` then only the measurement attributes that
are in ``attribute_keys`` will be used to identify the metric stream.
aggregation: This is a metric stream customizing attribute: the
aggregation instance to use when data is aggregated for the
corresponding metrics stream. If `None` an instance of
`DefaultAggregation` will be used.
This class is not intended to be subclassed by the user.
"""
_default_aggregation = DefaultAggregation()
def __init__(
self,
instrument_type: Optional[Type[Instrument]] = None,
instrument_name: Optional[str] = None,
meter_name: Optional[str] = None,
meter_version: Optional[str] = None,
meter_schema_url: Optional[str] = None,
name: Optional[str] = None,
description: Optional[str] = None,
attribute_keys: Optional[Set[str]] = None,
aggregation: Optional[Aggregation] = None,
):
if (
instrument_type
is instrument_name
is meter_name
is meter_version
is meter_schema_url
is None
):
raise Exception(
"Some instrument selection "
f"criteria must be provided for View {name}"
)
if (
name is not None
and instrument_name is not None
and ("*" in instrument_name or "?" in instrument_name)
):
raise Exception(
f"View {name} declared with wildcard " "characters in instrument_name"
)
# _name, _description, _aggregation and _attribute_keys will be
# accessed when instantiating a _ViewInstrumentMatch.
self._name = name
self._instrument_type = instrument_type
self._instrument_name = instrument_name
self._meter_name = meter_name
self._meter_version = meter_version
self._meter_schema_url = meter_schema_url
self._description = description
self._attribute_keys = attribute_keys
self._aggregation = aggregation or self._default_aggregation
# pylint: disable=too-many-return-statements
# pylint: disable=too-many-branches
@final
def _match(self, instrument: Instrument) -> bool:
if self._instrument_type is not None:
if not isinstance(instrument, self._instrument_type):
return False
if self._instrument_name is not None:
if not fnmatch(instrument.name, self._instrument_name):
return False
if self._meter_name is not None:
if instrument.instrumentation_scope.name != self._meter_name:
return False
if self._meter_version is not None:
if instrument.instrumentation_scope.version != self._meter_version:
return False
if self._meter_schema_url is not None:
if instrument.instrumentation_scope.schema_url != self._meter_schema_url:
return False
return True
@@ -0,0 +1,63 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from mysql.opentelemetry.sdk.metrics._internal.export import (
AggregationTemporality,
ConsoleMetricExporter,
InMemoryMetricReader,
MetricExporter,
MetricExportResult,
MetricReader,
PeriodicExportingMetricReader,
)
# The point module is not in the export directory to avoid a circular import.
from mysql.opentelemetry.sdk.metrics._internal.point import ( # noqa: F401
Buckets,
DataPointT,
DataT,
ExponentialHistogram,
ExponentialHistogramDataPoint,
Gauge,
Histogram,
HistogramDataPoint,
Metric,
MetricsData,
NumberDataPoint,
ResourceMetrics,
ScopeMetrics,
Sum,
)
__all__ = [
"AggregationTemporality",
"ConsoleMetricExporter",
"InMemoryMetricReader",
"MetricExporter",
"MetricExportResult",
"MetricReader",
"PeriodicExportingMetricReader",
"DataPointT",
"DataT",
"Gauge",
"Histogram",
"HistogramDataPoint",
"Metric",
"MetricsData",
"NumberDataPoint",
"ResourceMetrics",
"ScopeMetrics",
"Sum",
]
@@ -0,0 +1,35 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from mysql.opentelemetry.sdk.metrics._internal.aggregation import (
Aggregation,
DefaultAggregation,
DropAggregation,
ExplicitBucketHistogramAggregation,
ExponentialBucketHistogramAggregation,
LastValueAggregation,
SumAggregation,
)
from mysql.opentelemetry.sdk.metrics._internal.view import View
__all__ = [
"Aggregation",
"DefaultAggregation",
"DropAggregation",
"ExplicitBucketHistogramAggregation",
"ExponentialBucketHistogramAggregation",
"LastValueAggregation",
"SumAggregation",
"View",
]
View File
@@ -0,0 +1,378 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
This package implements `OpenTelemetry Resources
<https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md#resource-sdk>`_:
*A Resource is an immutable representation of the entity producing
telemetry. For example, a process producing telemetry that is running in
a container on Kubernetes has a Pod name, it is in a namespace and
possibly is part of a Deployment which also has a name. All three of
these attributes can be included in the Resource.*
Resource objects are created with `Resource.create`, which accepts attributes
(key-values). Resources should NOT be created via constructor, and working with
`Resource` objects should only be done via the Resource API methods. Resource
attributes can also be passed at process invocation in the
:envvar:`OTEL_RESOURCE_ATTRIBUTES` environment variable. You should register
your resource with the `mysql.opentelemetry.sdk.trace.TracerProvider` by passing
them into their constructors. The `Resource` passed to a provider is available
to the exporter, which can send on this information as it sees fit.
.. code-block:: python
trace.set_tracer_provider(
TracerProvider(
resource=Resource.create({
"service.name": "shoppingcart",
"service.instance.id": "instance-12",
}),
),
)
print(trace.get_tracer_provider().resource.attributes)
{'telemetry.sdk.language': 'python',
'telemetry.sdk.name': 'opentelemetry',
'telemetry.sdk.version': '0.13.dev0',
'service.name': 'shoppingcart',
'service.instance.id': 'instance-12'}
Note that the OpenTelemetry project documents certain `"standard attributes"
<https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/semantic_conventions/README.md>`_
that have prescribed semantic meanings, for example ``service.name`` in the
above example.
"""
import abc
import concurrent.futures
import logging
import sys
import typing
from json import dumps
from os import environ
from urllib import parse
from mysql.opentelemetry.attributes import BoundedAttributes
from mysql.opentelemetry.sdk.environment_variables import (
OTEL_EXPERIMENTAL_RESOURCE_DETECTORS,
OTEL_RESOURCE_ATTRIBUTES,
OTEL_SERVICE_NAME,
)
from mysql.opentelemetry.semconv.resource import ResourceAttributes
from mysql.opentelemetry.util._importlib_metadata import entry_points, version
from mysql.opentelemetry.util.types import AttributeValue
LabelValue = AttributeValue
Attributes = typing.Dict[str, LabelValue]
logger = logging.getLogger(__name__)
CLOUD_PROVIDER = ResourceAttributes.CLOUD_PROVIDER
CLOUD_ACCOUNT_ID = ResourceAttributes.CLOUD_ACCOUNT_ID
CLOUD_REGION = ResourceAttributes.CLOUD_REGION
CLOUD_AVAILABILITY_ZONE = ResourceAttributes.CLOUD_AVAILABILITY_ZONE
CONTAINER_NAME = ResourceAttributes.CONTAINER_NAME
CONTAINER_ID = ResourceAttributes.CONTAINER_ID
CONTAINER_IMAGE_NAME = ResourceAttributes.CONTAINER_IMAGE_NAME
CONTAINER_IMAGE_TAG = ResourceAttributes.CONTAINER_IMAGE_TAG
DEPLOYMENT_ENVIRONMENT = ResourceAttributes.DEPLOYMENT_ENVIRONMENT
FAAS_NAME = ResourceAttributes.FAAS_NAME
FAAS_ID = ResourceAttributes.FAAS_ID
FAAS_VERSION = ResourceAttributes.FAAS_VERSION
FAAS_INSTANCE = ResourceAttributes.FAAS_INSTANCE
HOST_NAME = ResourceAttributes.HOST_NAME
HOST_TYPE = ResourceAttributes.HOST_TYPE
HOST_IMAGE_NAME = ResourceAttributes.HOST_IMAGE_NAME
HOST_IMAGE_ID = ResourceAttributes.HOST_IMAGE_ID
HOST_IMAGE_VERSION = ResourceAttributes.HOST_IMAGE_VERSION
KUBERNETES_CLUSTER_NAME = ResourceAttributes.K8S_CLUSTER_NAME
KUBERNETES_NAMESPACE_NAME = ResourceAttributes.K8S_NAMESPACE_NAME
KUBERNETES_POD_UID = ResourceAttributes.K8S_POD_UID
KUBERNETES_POD_NAME = ResourceAttributes.K8S_POD_NAME
KUBERNETES_CONTAINER_NAME = ResourceAttributes.K8S_CONTAINER_NAME
KUBERNETES_REPLICA_SET_UID = ResourceAttributes.K8S_REPLICASET_UID
KUBERNETES_REPLICA_SET_NAME = ResourceAttributes.K8S_REPLICASET_NAME
KUBERNETES_DEPLOYMENT_UID = ResourceAttributes.K8S_DEPLOYMENT_UID
KUBERNETES_DEPLOYMENT_NAME = ResourceAttributes.K8S_DEPLOYMENT_NAME
KUBERNETES_STATEFUL_SET_UID = ResourceAttributes.K8S_STATEFULSET_UID
KUBERNETES_STATEFUL_SET_NAME = ResourceAttributes.K8S_STATEFULSET_NAME
KUBERNETES_DAEMON_SET_UID = ResourceAttributes.K8S_DAEMONSET_UID
KUBERNETES_DAEMON_SET_NAME = ResourceAttributes.K8S_DAEMONSET_NAME
KUBERNETES_JOB_UID = ResourceAttributes.K8S_JOB_UID
KUBERNETES_JOB_NAME = ResourceAttributes.K8S_JOB_NAME
KUBERNETES_CRON_JOB_UID = ResourceAttributes.K8S_CRONJOB_UID
KUBERNETES_CRON_JOB_NAME = ResourceAttributes.K8S_CRONJOB_NAME
OS_TYPE = ResourceAttributes.OS_TYPE
OS_DESCRIPTION = ResourceAttributes.OS_DESCRIPTION
PROCESS_PID = ResourceAttributes.PROCESS_PID
PROCESS_EXECUTABLE_NAME = ResourceAttributes.PROCESS_EXECUTABLE_NAME
PROCESS_EXECUTABLE_PATH = ResourceAttributes.PROCESS_EXECUTABLE_PATH
PROCESS_COMMAND = ResourceAttributes.PROCESS_COMMAND
PROCESS_COMMAND_LINE = ResourceAttributes.PROCESS_COMMAND_LINE
PROCESS_COMMAND_ARGS = ResourceAttributes.PROCESS_COMMAND_ARGS
PROCESS_OWNER = ResourceAttributes.PROCESS_OWNER
PROCESS_RUNTIME_NAME = ResourceAttributes.PROCESS_RUNTIME_NAME
PROCESS_RUNTIME_VERSION = ResourceAttributes.PROCESS_RUNTIME_VERSION
PROCESS_RUNTIME_DESCRIPTION = ResourceAttributes.PROCESS_RUNTIME_DESCRIPTION
SERVICE_NAME = ResourceAttributes.SERVICE_NAME
SERVICE_NAMESPACE = ResourceAttributes.SERVICE_NAMESPACE
SERVICE_INSTANCE_ID = ResourceAttributes.SERVICE_INSTANCE_ID
SERVICE_VERSION = ResourceAttributes.SERVICE_VERSION
TELEMETRY_SDK_NAME = ResourceAttributes.TELEMETRY_SDK_NAME
TELEMETRY_SDK_VERSION = ResourceAttributes.TELEMETRY_SDK_VERSION
TELEMETRY_AUTO_VERSION = ResourceAttributes.TELEMETRY_AUTO_VERSION
TELEMETRY_SDK_LANGUAGE = ResourceAttributes.TELEMETRY_SDK_LANGUAGE
_OPENTELEMETRY_SDK_VERSION = version("opentelemetry-sdk")
class Resource:
"""A Resource is an immutable representation of the entity producing telemetry as Attributes."""
def __init__(self, attributes: Attributes, schema_url: typing.Optional[str] = None):
self._attributes = BoundedAttributes(attributes=attributes)
if schema_url is None:
schema_url = ""
self._schema_url = schema_url
@staticmethod
def create(
attributes: typing.Optional[Attributes] = None,
schema_url: typing.Optional[str] = None,
) -> "Resource":
"""Creates a new `Resource` from attributes.
Args:
attributes: Optional zero or more key-value pairs.
schema_url: Optional URL pointing to the schema
Returns:
The newly-created Resource.
"""
if not attributes:
attributes = {}
resource_detectors = []
resource = _DEFAULT_RESOURCE
otel_experimental_resource_detectors = environ.get(
OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, "otel"
).split(",")
if "otel" not in otel_experimental_resource_detectors:
otel_experimental_resource_detectors.append("otel")
for resource_detector in otel_experimental_resource_detectors:
resource_detectors.append(
next(
iter(
entry_points(
group="opentelemetry_resource_detector",
name=resource_detector.strip(),
)
)
).load()()
)
resource = get_aggregated_resources(
resource_detectors, _DEFAULT_RESOURCE
).merge(Resource(attributes, schema_url))
if not resource.attributes.get(SERVICE_NAME, None):
default_service_name = "unknown_service"
process_executable_name = resource.attributes.get(
PROCESS_EXECUTABLE_NAME, None
)
if process_executable_name:
default_service_name += ":" + process_executable_name
resource = resource.merge(
Resource({SERVICE_NAME: default_service_name}, schema_url)
)
return resource
@staticmethod
def get_empty() -> "Resource":
return _EMPTY_RESOURCE
@property
def attributes(self) -> Attributes:
return self._attributes
@property
def schema_url(self) -> str:
return self._schema_url
def merge(self, other: "Resource") -> "Resource":
"""Merges this resource and an updating resource into a new `Resource`.
If a key exists on both the old and updating resource, the value of the
updating resource will override the old resource value.
The updating resource's `schema_url` will be used only if the old
`schema_url` is empty. Attempting to merge two resources with
different, non-empty values for `schema_url` will result in an error
and return the old resource.
Args:
other: The other resource to be merged.
Returns:
The newly-created Resource.
"""
merged_attributes = self.attributes.copy()
merged_attributes.update(other.attributes)
if self.schema_url == "":
schema_url = other.schema_url
elif other.schema_url == "":
schema_url = self.schema_url
elif self.schema_url == other.schema_url:
schema_url = other.schema_url
else:
logger.error(
"Failed to merge resources: The two schemas %s and %s are incompatible",
self.schema_url,
other.schema_url,
)
return self
return Resource(merged_attributes, schema_url)
def __eq__(self, other: object) -> bool:
if not isinstance(other, Resource):
return False
return (
self._attributes == other._attributes
and self._schema_url == other._schema_url
)
def __hash__(self):
return hash(
f"{dumps(self._attributes.copy(), sort_keys=True)}|{self._schema_url}"
)
def to_json(self, indent=4) -> str:
return dumps(
{
"attributes": dict(self._attributes),
"schema_url": self._schema_url,
},
indent=indent,
)
_EMPTY_RESOURCE = Resource({})
_DEFAULT_RESOURCE = Resource(
{
TELEMETRY_SDK_LANGUAGE: "python",
TELEMETRY_SDK_NAME: "opentelemetry",
TELEMETRY_SDK_VERSION: _OPENTELEMETRY_SDK_VERSION,
}
)
class ResourceDetector(abc.ABC):
def __init__(self, raise_on_error=False):
self.raise_on_error = raise_on_error
@abc.abstractmethod
def detect(self) -> "Resource":
raise NotImplementedError()
class OTELResourceDetector(ResourceDetector):
# pylint: disable=no-self-use
def detect(self) -> "Resource":
env_resources_items = environ.get(OTEL_RESOURCE_ATTRIBUTES)
env_resource_map = {}
if env_resources_items:
for item in env_resources_items.split(","):
try:
key, value = item.split("=", maxsplit=1)
except ValueError as exc:
logger.warning(
"Invalid key value resource attribute pair %s: %s",
item,
exc,
)
continue
value_url_decoded = parse.unquote(value.strip())
env_resource_map[key.strip()] = value_url_decoded
service_name = environ.get(OTEL_SERVICE_NAME)
if service_name:
env_resource_map[SERVICE_NAME] = service_name
return Resource(env_resource_map)
class ProcessResourceDetector(ResourceDetector):
# pylint: disable=no-self-use
def detect(self) -> "Resource":
_runtime_version = ".".join(
map(
str,
sys.version_info[:3]
if sys.version_info.releaselevel == "final"
and not sys.version_info.serial
else sys.version_info,
)
)
return Resource(
{
PROCESS_RUNTIME_DESCRIPTION: sys.version,
PROCESS_RUNTIME_NAME: sys.implementation.name,
PROCESS_RUNTIME_VERSION: _runtime_version,
}
)
def get_aggregated_resources(
detectors: typing.List["ResourceDetector"],
initial_resource: typing.Optional[Resource] = None,
timeout=5,
) -> "Resource":
"""Retrieves resources from detectors in the order that they were passed
:param detectors: List of resources in order of priority
:param initial_resource: Static resource. This has highest priority
:param timeout: Number of seconds to wait for each detector to return
:return:
"""
detectors_merged_resource = initial_resource or Resource.create()
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
futures = [executor.submit(detector.detect) for detector in detectors]
for detector_ind, future in enumerate(futures):
detector = detectors[detector_ind]
try:
detected_resource = future.result(timeout=timeout)
# pylint: disable=broad-except
except Exception as ex:
detected_resource = _EMPTY_RESOURCE
if detector.raise_on_error:
raise ex
logger.warning("Exception %s in detector %s, ignoring", ex, detector)
finally:
detectors_merged_resource = detectors_merged_resource.merge(
detected_resource
)
return detectors_merged_resource
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,506 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import collections
import logging
import os
import sys
import threading
import typing
from enum import Enum
from os import environ, linesep
from time import time_ns
from typing import Optional
from mysql.opentelemetry.context import (
_SUPPRESS_INSTRUMENTATION_KEY,
Context,
attach,
detach,
set_value,
)
from mysql.opentelemetry.sdk.environment_variables import (
OTEL_BSP_EXPORT_TIMEOUT,
OTEL_BSP_MAX_EXPORT_BATCH_SIZE,
OTEL_BSP_MAX_QUEUE_SIZE,
OTEL_BSP_SCHEDULE_DELAY,
)
from mysql.opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor
from mysql.opentelemetry.util._once import Once
_DEFAULT_SCHEDULE_DELAY_MILLIS = 5000
_DEFAULT_MAX_EXPORT_BATCH_SIZE = 512
_DEFAULT_EXPORT_TIMEOUT_MILLIS = 30000
_DEFAULT_MAX_QUEUE_SIZE = 2048
_ENV_VAR_INT_VALUE_ERROR_MESSAGE = (
"Unable to parse value for %s as integer. Defaulting to %s."
)
logger = logging.getLogger(__name__)
class SpanExportResult(Enum):
SUCCESS = 0
FAILURE = 1
class SpanExporter:
"""Interface for exporting spans.
Interface to be implemented by services that want to export spans recorded
in their own format.
To export data this MUST be registered to the :class`mysql.opentelemetry.sdk.trace.Tracer` using a
`SimpleSpanProcessor` or a `BatchSpanProcessor`.
"""
def export(self, spans: typing.Sequence[ReadableSpan]) -> "SpanExportResult":
"""Exports a batch of telemetry data.
Args:
spans: The list of `mysql.opentelemetry.trace.Span` objects to be exported
Returns:
The result of the export
"""
def shutdown(self) -> None:
"""Shuts down the exporter.
Called when the SDK is shut down.
"""
def force_flush(self, timeout_millis: int = 30000) -> bool:
"""Hint to ensure that the export of any spans the exporter has received
prior to the call to ForceFlush SHOULD be completed as soon as possible, preferably
before returning from this method.
"""
class SimpleSpanProcessor(SpanProcessor):
"""Simple SpanProcessor implementation.
SimpleSpanProcessor is an implementation of `SpanProcessor` that
passes ended spans directly to the configured `SpanExporter`.
"""
def __init__(self, span_exporter: SpanExporter):
self.span_exporter = span_exporter
def on_start(
self, span: Span, parent_context: typing.Optional[Context] = None
) -> None:
pass
def on_end(self, span: ReadableSpan) -> None:
if not span.context.trace_flags.sampled:
return
token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True))
try:
self.span_exporter.export((span,))
# pylint: disable=broad-except
except Exception:
logger.exception("Exception while exporting Span.")
detach(token)
def shutdown(self) -> None:
self.span_exporter.shutdown()
def force_flush(self, timeout_millis: int = 30000) -> bool:
# pylint: disable=unused-argument
return True
class _FlushRequest:
"""Represents a request for the BatchSpanProcessor to flush spans."""
__slots__ = ["event", "num_spans"]
def __init__(self):
self.event = threading.Event()
self.num_spans = 0
_BSP_RESET_ONCE = Once()
class BatchSpanProcessor(SpanProcessor):
"""Batch span processor implementation.
`BatchSpanProcessor` is an implementation of `SpanProcessor` that
batches ended spans and pushes them to the configured `SpanExporter`.
`BatchSpanProcessor` is configurable with the following environment
variables which correspond to constructor parameters:
- :envvar:`OTEL_BSP_SCHEDULE_DELAY`
- :envvar:`OTEL_BSP_MAX_QUEUE_SIZE`
- :envvar:`OTEL_BSP_MAX_EXPORT_BATCH_SIZE`
- :envvar:`OTEL_BSP_EXPORT_TIMEOUT`
"""
def __init__(
self,
span_exporter: SpanExporter,
max_queue_size: int = None,
schedule_delay_millis: float = None,
max_export_batch_size: int = None,
export_timeout_millis: float = None,
):
if max_queue_size is None:
max_queue_size = BatchSpanProcessor._default_max_queue_size()
if schedule_delay_millis is None:
schedule_delay_millis = BatchSpanProcessor._default_schedule_delay_millis()
if max_export_batch_size is None:
max_export_batch_size = BatchSpanProcessor._default_max_export_batch_size()
if export_timeout_millis is None:
export_timeout_millis = BatchSpanProcessor._default_export_timeout_millis()
BatchSpanProcessor._validate_arguments(
max_queue_size, schedule_delay_millis, max_export_batch_size
)
self.span_exporter = span_exporter
self.queue = collections.deque([], max_queue_size) # type: typing.Deque[Span]
self.worker_thread = threading.Thread(
name="OtelBatchSpanProcessor", target=self.worker, daemon=True
)
self.condition = threading.Condition(threading.Lock())
self._flush_request = None # type: typing.Optional[_FlushRequest]
self.schedule_delay_millis = schedule_delay_millis
self.max_export_batch_size = max_export_batch_size
self.max_queue_size = max_queue_size
self.export_timeout_millis = export_timeout_millis
self.done = False
# flag that indicates that spans are being dropped
self._spans_dropped = False
# precallocated list to send spans to exporter
self.spans_list = [
None
] * self.max_export_batch_size # type: typing.List[typing.Optional[Span]]
self.worker_thread.start()
# Only available in *nix since py37.
if hasattr(os, "register_at_fork"):
os.register_at_fork(
after_in_child=self._at_fork_reinit
) # pylint: disable=protected-access
self._pid = os.getpid()
def on_start(
self, span: Span, parent_context: typing.Optional[Context] = None
) -> None:
pass
def on_end(self, span: ReadableSpan) -> None:
if self.done:
logger.warning("Already shutdown, dropping span.")
return
if not span.context.trace_flags.sampled:
return
if self._pid != os.getpid():
_BSP_RESET_ONCE.do_once(self._at_fork_reinit)
if len(self.queue) == self.max_queue_size:
if not self._spans_dropped:
logger.warning("Queue is full, likely spans will be dropped.")
self._spans_dropped = True
self.queue.appendleft(span)
if len(self.queue) >= self.max_export_batch_size:
with self.condition:
self.condition.notify()
def _at_fork_reinit(self):
self.condition = threading.Condition(threading.Lock())
self.queue.clear()
# worker_thread is local to a process, only the thread that issued fork continues
# to exist. A new worker thread must be started in child process.
self.worker_thread = threading.Thread(
name="OtelBatchSpanProcessor", target=self.worker, daemon=True
)
self.worker_thread.start()
self._pid = os.getpid()
def worker(self):
timeout = self.schedule_delay_millis / 1e3
flush_request = None # type: typing.Optional[_FlushRequest]
while not self.done:
with self.condition:
if self.done:
# done flag may have changed, avoid waiting
break
flush_request = self._get_and_unset_flush_request()
if (
len(self.queue) < self.max_export_batch_size
and flush_request is None
):
self.condition.wait(timeout)
flush_request = self._get_and_unset_flush_request()
if not self.queue:
# spurious notification, let's wait again, reset timeout
timeout = self.schedule_delay_millis / 1e3
self._notify_flush_request_finished(flush_request)
flush_request = None
continue
if self.done:
# missing spans will be sent when calling flush
break
# subtract the duration of this export call to the next timeout
start = time_ns()
self._export(flush_request)
end = time_ns()
duration = (end - start) / 1e9
timeout = self.schedule_delay_millis / 1e3 - duration
self._notify_flush_request_finished(flush_request)
flush_request = None
# there might have been a new flush request while export was running
# and before the done flag switched to true
with self.condition:
shutdown_flush_request = self._get_and_unset_flush_request()
# be sure that all spans are sent
self._drain_queue()
self._notify_flush_request_finished(flush_request)
self._notify_flush_request_finished(shutdown_flush_request)
def _get_and_unset_flush_request(
self,
) -> typing.Optional[_FlushRequest]:
"""Returns the current flush request and makes it invisible to the
worker thread for subsequent calls.
"""
flush_request = self._flush_request
self._flush_request = None
if flush_request is not None:
flush_request.num_spans = len(self.queue)
return flush_request
@staticmethod
def _notify_flush_request_finished(
flush_request: typing.Optional[_FlushRequest],
):
"""Notifies the flush initiator(s) waiting on the given request/event
that the flush operation was finished.
"""
if flush_request is not None:
flush_request.event.set()
def _get_or_create_flush_request(self) -> _FlushRequest:
"""Either returns the current active flush event or creates a new one.
The flush event will be visible and read by the worker thread before an
export operation starts. Callers of a flush operation may wait on the
returned event to be notified when the flush/export operation was
finished.
This method is not thread-safe, i.e. callers need to take care about
synchronization/locking.
"""
if self._flush_request is None:
self._flush_request = _FlushRequest()
return self._flush_request
def _export(self, flush_request: typing.Optional[_FlushRequest]):
"""Exports spans considering the given flush_request.
In case of a given flush_requests spans are exported in batches until
the number of exported spans reached or exceeded the number of spans in
the flush request.
In no flush_request was given at most max_export_batch_size spans are
exported.
"""
if not flush_request:
self._export_batch()
return
num_spans = flush_request.num_spans
while self.queue:
num_exported = self._export_batch()
num_spans -= num_exported
if num_spans <= 0:
break
def _export_batch(self) -> int:
"""Exports at most max_export_batch_size spans and returns the number of
exported spans.
"""
idx = 0
# currently only a single thread acts as consumer, so queue.pop() will
# not raise an exception
while idx < self.max_export_batch_size and self.queue:
self.spans_list[idx] = self.queue.pop()
idx += 1
token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True))
try:
# Ignore type b/c the Optional[None]+slicing is too "clever"
# for mypy
self.span_exporter.export(self.spans_list[:idx]) # type: ignore
except Exception: # pylint: disable=broad-except
logger.exception("Exception while exporting Span batch.")
detach(token)
# clean up list
for index in range(idx):
self.spans_list[index] = None
return idx
def _drain_queue(self):
"""Export all elements until queue is empty.
Can only be called from the worker thread context because it invokes
`export` that is not thread safe.
"""
while self.queue:
self._export_batch()
def force_flush(self, timeout_millis: int = None) -> bool:
if timeout_millis is None:
timeout_millis = self.export_timeout_millis
if self.done:
logger.warning("Already shutdown, ignoring call to force_flush().")
return True
with self.condition:
flush_request = self._get_or_create_flush_request()
# signal the worker thread to flush and wait for it to finish
self.condition.notify_all()
# wait for token to be processed
ret = flush_request.event.wait(timeout_millis / 1e3)
if not ret:
logger.warning("Timeout was exceeded in force_flush().")
return ret
def shutdown(self) -> None:
# signal the worker thread to finish and then wait for it
self.done = True
with self.condition:
self.condition.notify_all()
self.worker_thread.join()
self.span_exporter.shutdown()
@staticmethod
def _default_max_queue_size():
try:
return int(environ.get(OTEL_BSP_MAX_QUEUE_SIZE, _DEFAULT_MAX_QUEUE_SIZE))
except ValueError:
logger.exception(
_ENV_VAR_INT_VALUE_ERROR_MESSAGE,
OTEL_BSP_MAX_QUEUE_SIZE,
_DEFAULT_MAX_QUEUE_SIZE,
)
return _DEFAULT_MAX_QUEUE_SIZE
@staticmethod
def _default_schedule_delay_millis():
try:
return int(
environ.get(OTEL_BSP_SCHEDULE_DELAY, _DEFAULT_SCHEDULE_DELAY_MILLIS)
)
except ValueError:
logger.exception(
_ENV_VAR_INT_VALUE_ERROR_MESSAGE,
OTEL_BSP_SCHEDULE_DELAY,
_DEFAULT_SCHEDULE_DELAY_MILLIS,
)
return _DEFAULT_SCHEDULE_DELAY_MILLIS
@staticmethod
def _default_max_export_batch_size():
try:
return int(
environ.get(
OTEL_BSP_MAX_EXPORT_BATCH_SIZE,
_DEFAULT_MAX_EXPORT_BATCH_SIZE,
)
)
except ValueError:
logger.exception(
_ENV_VAR_INT_VALUE_ERROR_MESSAGE,
OTEL_BSP_MAX_EXPORT_BATCH_SIZE,
_DEFAULT_MAX_EXPORT_BATCH_SIZE,
)
return _DEFAULT_MAX_EXPORT_BATCH_SIZE
@staticmethod
def _default_export_timeout_millis():
try:
return int(
environ.get(OTEL_BSP_EXPORT_TIMEOUT, _DEFAULT_EXPORT_TIMEOUT_MILLIS)
)
except ValueError:
logger.exception(
_ENV_VAR_INT_VALUE_ERROR_MESSAGE,
OTEL_BSP_EXPORT_TIMEOUT,
_DEFAULT_EXPORT_TIMEOUT_MILLIS,
)
return _DEFAULT_EXPORT_TIMEOUT_MILLIS
@staticmethod
def _validate_arguments(
max_queue_size, schedule_delay_millis, max_export_batch_size
):
if max_queue_size <= 0:
raise ValueError("max_queue_size must be a positive integer.")
if schedule_delay_millis <= 0:
raise ValueError("schedule_delay_millis must be positive.")
if max_export_batch_size <= 0:
raise ValueError("max_export_batch_size must be a positive integer.")
if max_export_batch_size > max_queue_size:
raise ValueError(
"max_export_batch_size must be less than or equal to max_queue_size."
)
class ConsoleSpanExporter(SpanExporter):
"""Implementation of :class:`SpanExporter` that prints spans to the
console.
This class can be used for diagnostic purposes. It prints the exported
spans to the console STDOUT.
"""
def __init__(
self,
service_name: Optional[str] = None,
out: typing.IO = sys.stdout,
formatter: typing.Callable[[ReadableSpan], str] = lambda span: span.to_json()
+ linesep,
):
self.out = out
self.formatter = formatter
self.service_name = service_name
def export(self, spans: typing.Sequence[ReadableSpan]) -> SpanExportResult:
for span in spans:
self.out.write(self.formatter(span))
self.out.flush()
return SpanExportResult.SUCCESS
def force_flush(self, timeout_millis: int = 30000) -> bool:
return True
@@ -0,0 +1,61 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import threading
import typing
from mysql.opentelemetry.sdk.trace import ReadableSpan
from mysql.opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
class InMemorySpanExporter(SpanExporter):
"""Implementation of :class:`.SpanExporter` that stores spans in memory.
This class can be used for testing purposes. It stores the exported spans
in a list in memory that can be retrieved using the
:func:`.get_finished_spans` method.
"""
def __init__(self):
self._finished_spans = []
self._stopped = False
self._lock = threading.Lock()
def clear(self):
"""Clear list of collected spans."""
with self._lock:
self._finished_spans.clear()
def get_finished_spans(self):
"""Get list of collected spans."""
with self._lock:
return tuple(self._finished_spans)
def export(self, spans: typing.Sequence[ReadableSpan]) -> SpanExportResult:
"""Stores a list of spans in memory."""
if self._stopped:
return SpanExportResult.FAILURE
with self._lock:
self._finished_spans.extend(spans)
return SpanExportResult.SUCCESS
def shutdown(self):
"""Shut downs the exporter.
Calls to export after the exporter has been shut down will fail.
"""
self._stopped = True
def force_flush(self, timeout_millis: int = 30000) -> bool:
return True
@@ -0,0 +1,52 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import abc
import random
class IdGenerator(abc.ABC):
@abc.abstractmethod
def generate_span_id(self) -> int:
"""Get a new span ID.
Returns:
A 64-bit int for use as a span ID
"""
@abc.abstractmethod
def generate_trace_id(self) -> int:
"""Get a new trace ID.
Implementations should at least make the 64 least significant bits
uniformly random. Samplers like the `TraceIdRatioBased` sampler rely on
this randomness to make sampling decisions.
See `the specification on TraceIdRatioBased <https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#traceidratiobased>`_.
Returns:
A 128-bit int for use as a trace ID
"""
class RandomIdGenerator(IdGenerator):
"""The default ID generator for TracerProvider which randomly generates all
bits when generating IDs.
"""
def generate_span_id(self) -> int:
return random.getrandbits(64)
def generate_trace_id(self) -> int:
return random.getrandbits(128)
+447
View File
@@ -0,0 +1,447 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
For general information about sampling, see `the specification <https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#sampling>`_.
OpenTelemetry provides two types of samplers:
- `StaticSampler`
- `TraceIdRatioBased`
A `StaticSampler` always returns the same sampling result regardless of the conditions. Both possible StaticSamplers are already created:
- Always sample spans: ALWAYS_ON
- Never sample spans: ALWAYS_OFF
A `TraceIdRatioBased` sampler makes a random sampling result based on the sampling probability given.
If the span being sampled has a parent, `ParentBased` will respect the parent delegate sampler. Otherwise, it returns the sampling result from the given root sampler.
Currently, sampling results are always made during the creation of the span. However, this might not always be the case in the future (see `OTEP #115 <https://github.com/open-telemetry/oteps/pull/115>`_).
Custom samplers can be created by subclassing `Sampler` and implementing `Sampler.should_sample` as well as `Sampler.get_description`.
Samplers are able to modify the `mysql.opentelemetry.trace.span.TraceState` of the parent of the span being created. For custom samplers, it is suggested to implement `Sampler.should_sample` to utilize the
parent span context's `mysql.opentelemetry.trace.span.TraceState` and pass into the `SamplingResult` instead of the explicit trace_state field passed into the parameter of `Sampler.should_sample`.
To use a sampler, pass it into the tracer provider constructor. For example:
.. code:: python
from opentelemetry import trace
from mysql.opentelemetry.sdk.trace import TracerProvider
from mysql.opentelemetry.sdk.trace.export import (
ConsoleSpanExporter,
SimpleSpanProcessor,
)
from mysql.opentelemetry.sdk.trace.sampling import TraceIdRatioBased
# sample 1 in every 1000 traces
sampler = TraceIdRatioBased(1/1000)
# set the sampler onto the global tracer provider
trace.set_tracer_provider(TracerProvider(sampler=sampler))
# set up an exporter for sampled spans
trace.get_tracer_provider().add_span_processor(
SimpleSpanProcessor(ConsoleSpanExporter())
)
# created spans will now be sampled by the TraceIdRatioBased sampler
with trace.get_tracer(__name__).start_as_current_span("Test Span"):
...
The tracer sampler can also be configured via environment variables ``OTEL_TRACES_SAMPLER`` and ``OTEL_TRACES_SAMPLER_ARG`` (only if applicable).
The list of built-in values for ``OTEL_TRACES_SAMPLER`` are:
* always_on - Sampler that always samples spans, regardless of the parent span's sampling decision.
* always_off - Sampler that never samples spans, regardless of the parent span's sampling decision.
* traceidratio - Sampler that samples probabalistically based on rate.
* parentbased_always_on - (default) Sampler that respects its parent span's sampling decision, but otherwise always samples.
* parentbased_always_off - Sampler that respects its parent span's sampling decision, but otherwise never samples.
* parentbased_traceidratio - Sampler that respects its parent span's sampling decision, but otherwise samples probabalistically based on rate.
Sampling probability can be set with ``OTEL_TRACES_SAMPLER_ARG`` if the sampler is traceidratio or parentbased_traceidratio. Rate must be in the range [0.0,1.0]. When not provided rate will be set to
1.0 (maximum rate possible).
Prev example but with environment variables. Please make sure to set the env ``OTEL_TRACES_SAMPLER=traceidratio`` and ``OTEL_TRACES_SAMPLER_ARG=0.001``.
.. code:: python
from opentelemetry import trace
from mysql.opentelemetry.sdk.trace import TracerProvider
from mysql.opentelemetry.sdk.trace.export import (
ConsoleSpanExporter,
SimpleSpanProcessor,
)
trace.set_tracer_provider(TracerProvider())
# set up an exporter for sampled spans
trace.get_tracer_provider().add_span_processor(
SimpleSpanProcessor(ConsoleSpanExporter())
)
# created spans will now be sampled by the TraceIdRatioBased sampler with rate 1/1000.
with trace.get_tracer(__name__).start_as_current_span("Test Span"):
...
When utilizing a configurator, you can configure a custom sampler. In order to create a configurable custom sampler, create an entry point for the custom sampler
factory method or function under the entry point group, ``opentelemetry_traces_sampler``. The custom sampler factory method must be of type ``Callable[[str], Sampler]``, taking a single string argument and
returning a Sampler object. The single input will come from the string value of the ``OTEL_TRACES_SAMPLER_ARG`` environment variable. If ``OTEL_TRACES_SAMPLER_ARG`` is not configured, the input will
be an empty string. For example:
.. code:: python
setup(
...
entry_points={
...
"opentelemetry_traces_sampler": [
"custom_sampler_name = path.to.sampler.factory.method:CustomSamplerFactory.get_sampler"
]
}
)
# ...
class CustomRatioSampler(Sampler):
def __init__(rate):
# ...
# ...
class CustomSamplerFactory:
@staticmethod
get_sampler(sampler_argument):
try:
rate = float(sampler_argument)
return CustomSampler(rate)
except ValueError: # In case argument is empty string.
return CustomSampler(0.5)
In order to configure you application with a custom sampler's entry point, set the ``OTEL_TRACES_SAMPLER`` environment variable to the key name of the entry point. For example, to configured the
above sampler, set ``OTEL_TRACES_SAMPLER=custom_sampler_name`` and ``OTEL_TRACES_SAMPLER_ARG=0.5``.
"""
import abc
import enum
import os
from logging import getLogger
from types import MappingProxyType
from typing import Optional, Sequence
# pylint: disable=unused-import
from mysql.opentelemetry.context import Context
from mysql.opentelemetry.sdk.environment_variables import (
OTEL_TRACES_SAMPLER,
OTEL_TRACES_SAMPLER_ARG,
)
from mysql.opentelemetry.trace import Link, SpanKind, get_current_span
from mysql.opentelemetry.trace.span import TraceState
from mysql.opentelemetry.util.types import Attributes
_logger = getLogger(__name__)
class Decision(enum.Enum):
# IsRecording() == false, span will not be recorded and all events and attributes will be dropped.
DROP = 0
# IsRecording() == true, but Sampled flag MUST NOT be set.
RECORD_ONLY = 1
# IsRecording() == true AND Sampled flag` MUST be set.
RECORD_AND_SAMPLE = 2
def is_recording(self):
return self in (Decision.RECORD_ONLY, Decision.RECORD_AND_SAMPLE)
def is_sampled(self):
return self is Decision.RECORD_AND_SAMPLE
class SamplingResult:
"""A sampling result as applied to a newly-created Span.
Args:
decision: A sampling decision based off of whether the span is recorded
and the sampled flag in trace flags in the span context.
attributes: Attributes to add to the `mysql.opentelemetry.trace.Span`.
trace_state: The tracestate used for the `mysql.opentelemetry.trace.Span`.
Could possibly have been modified by the sampler.
"""
def __repr__(self) -> str:
return f"{type(self).__name__}({str(self.decision)}, attributes={str(self.attributes)})"
def __init__(
self,
decision: Decision,
attributes: "Attributes" = None,
trace_state: "TraceState" = None,
) -> None:
self.decision = decision
if attributes is None:
self.attributes = MappingProxyType({})
else:
self.attributes = MappingProxyType(attributes)
self.trace_state = trace_state
class Sampler(abc.ABC):
@abc.abstractmethod
def should_sample(
self,
parent_context: Optional["Context"],
trace_id: int,
name: str,
kind: SpanKind = None,
attributes: Attributes = None,
links: Sequence["Link"] = None,
trace_state: "TraceState" = None,
) -> "SamplingResult":
pass
@abc.abstractmethod
def get_description(self) -> str:
pass
class StaticSampler(Sampler):
"""Sampler that always returns the same decision."""
def __init__(self, decision: "Decision"):
self._decision = decision
def should_sample(
self,
parent_context: Optional["Context"],
trace_id: int,
name: str,
kind: SpanKind = None,
attributes: Attributes = None,
links: Sequence["Link"] = None,
trace_state: "TraceState" = None,
) -> "SamplingResult":
if self._decision is Decision.DROP:
attributes = None
return SamplingResult(
self._decision,
attributes,
_get_parent_trace_state(parent_context),
)
def get_description(self) -> str:
if self._decision is Decision.DROP:
return "AlwaysOffSampler"
return "AlwaysOnSampler"
ALWAYS_OFF = StaticSampler(Decision.DROP)
"""Sampler that never samples spans, regardless of the parent span's sampling decision."""
ALWAYS_ON = StaticSampler(Decision.RECORD_AND_SAMPLE)
"""Sampler that always samples spans, regardless of the parent span's sampling decision."""
class TraceIdRatioBased(Sampler):
"""
Sampler that makes sampling decisions probabilistically based on `rate`.
Args:
rate: Probability (between 0 and 1) that a span will be sampled
"""
def __init__(self, rate: float):
if rate < 0.0 or rate > 1.0:
raise ValueError("Probability must be in range [0.0, 1.0].")
self._rate = rate
self._bound = self.get_bound_for_rate(self._rate)
# For compatibility with 64 bit trace IDs, the sampler checks the 64
# low-order bits of the trace ID to decide whether to sample a given trace.
TRACE_ID_LIMIT = (1 << 64) - 1
@classmethod
def get_bound_for_rate(cls, rate: float) -> int:
return round(rate * (cls.TRACE_ID_LIMIT + 1))
@property
def rate(self) -> float:
return self._rate
@property
def bound(self) -> int:
return self._bound
def should_sample(
self,
parent_context: Optional["Context"],
trace_id: int,
name: str,
kind: SpanKind = None,
attributes: Attributes = None,
links: Sequence["Link"] = None,
trace_state: "TraceState" = None,
) -> "SamplingResult":
decision = Decision.DROP
if trace_id & self.TRACE_ID_LIMIT < self.bound:
decision = Decision.RECORD_AND_SAMPLE
if decision is Decision.DROP:
attributes = None
return SamplingResult(
decision,
attributes,
_get_parent_trace_state(parent_context),
)
def get_description(self) -> str:
return f"TraceIdRatioBased{{{self._rate}}}"
class ParentBased(Sampler):
"""
If a parent is set, applies the respective delegate sampler.
Otherwise, uses the root provided at initialization to make a
decision.
Args:
root: Sampler called for spans with no parent (root spans).
remote_parent_sampled: Sampler called for a remote sampled parent.
remote_parent_not_sampled: Sampler called for a remote parent that is
not sampled.
local_parent_sampled: Sampler called for a local sampled parent.
local_parent_not_sampled: Sampler called for a local parent that is
not sampled.
"""
def __init__(
self,
root: Sampler,
remote_parent_sampled: Sampler = ALWAYS_ON,
remote_parent_not_sampled: Sampler = ALWAYS_OFF,
local_parent_sampled: Sampler = ALWAYS_ON,
local_parent_not_sampled: Sampler = ALWAYS_OFF,
):
self._root = root
self._remote_parent_sampled = remote_parent_sampled
self._remote_parent_not_sampled = remote_parent_not_sampled
self._local_parent_sampled = local_parent_sampled
self._local_parent_not_sampled = local_parent_not_sampled
def should_sample(
self,
parent_context: Optional["Context"],
trace_id: int,
name: str,
kind: SpanKind = None,
attributes: Attributes = None,
links: Sequence["Link"] = None,
trace_state: "TraceState" = None,
) -> "SamplingResult":
parent_span_context = get_current_span(parent_context).get_span_context()
# default to the root sampler
sampler = self._root
# respect the sampling and remote flag of the parent if present
if parent_span_context is not None and parent_span_context.is_valid:
if parent_span_context.is_remote:
if parent_span_context.trace_flags.sampled:
sampler = self._remote_parent_sampled
else:
sampler = self._remote_parent_not_sampled
else:
if parent_span_context.trace_flags.sampled:
sampler = self._local_parent_sampled
else:
sampler = self._local_parent_not_sampled
return sampler.should_sample(
parent_context=parent_context,
trace_id=trace_id,
name=name,
kind=kind,
attributes=attributes,
links=links,
)
def get_description(self):
return f"ParentBased{{root:{self._root.get_description()},remoteParentSampled:{self._remote_parent_sampled.get_description()},remoteParentNotSampled:{self._remote_parent_not_sampled.get_description()},localParentSampled:{self._local_parent_sampled.get_description()},localParentNotSampled:{self._local_parent_not_sampled.get_description()}}}"
DEFAULT_OFF = ParentBased(ALWAYS_OFF)
"""Sampler that respects its parent span's sampling decision, but otherwise never samples."""
DEFAULT_ON = ParentBased(ALWAYS_ON)
"""Sampler that respects its parent span's sampling decision, but otherwise always samples."""
class ParentBasedTraceIdRatio(ParentBased):
"""
Sampler that respects its parent span's sampling decision, but otherwise
samples probabalistically based on `rate`.
"""
def __init__(self, rate: float):
root = TraceIdRatioBased(rate=rate)
super().__init__(root=root)
class _AlwaysOff(StaticSampler):
def __init__(self, _):
super().__init__(Decision.DROP)
class _AlwaysOn(StaticSampler):
def __init__(self, _):
super().__init__(Decision.RECORD_AND_SAMPLE)
class _ParentBasedAlwaysOff(ParentBased):
def __init__(self, _):
super().__init__(ALWAYS_OFF)
class _ParentBasedAlwaysOn(ParentBased):
def __init__(self, _):
super().__init__(ALWAYS_ON)
_KNOWN_SAMPLERS = {
"always_on": ALWAYS_ON,
"always_off": ALWAYS_OFF,
"parentbased_always_on": DEFAULT_ON,
"parentbased_always_off": DEFAULT_OFF,
"traceidratio": TraceIdRatioBased,
"parentbased_traceidratio": ParentBasedTraceIdRatio,
}
def _get_from_env_or_default() -> Sampler:
trace_sampler = os.getenv(OTEL_TRACES_SAMPLER, "parentbased_always_on").lower()
if trace_sampler not in _KNOWN_SAMPLERS:
_logger.warning("Couldn't recognize sampler %s.", trace_sampler)
trace_sampler = "parentbased_always_on"
if trace_sampler in ("traceidratio", "parentbased_traceidratio"):
try:
rate = float(os.getenv(OTEL_TRACES_SAMPLER_ARG))
except (ValueError, TypeError):
_logger.warning("Could not convert TRACES_SAMPLER_ARG to float.")
rate = 1.0
return _KNOWN_SAMPLERS[trace_sampler](rate)
return _KNOWN_SAMPLERS[trace_sampler]
def _get_parent_trace_state(parent_context) -> Optional["TraceState"]:
parent_span_context = get_current_span(parent_context).get_span_context()
if parent_span_context is None or not parent_span_context.is_valid:
return None
return parent_span_context.trace_state
+144
View File
@@ -0,0 +1,144 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import datetime
import threading
from collections import OrderedDict, deque
from collections.abc import MutableMapping, Sequence
from typing import Optional
from deprecated import deprecated
def ns_to_iso_str(nanoseconds):
"""Get an ISO 8601 string from time_ns value."""
ts = datetime.datetime.utcfromtimestamp(nanoseconds / 1e9)
return ts.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
def get_dict_as_key(labels):
"""Converts a dict to be used as a unique key"""
return tuple(
sorted(
map(
lambda kv: (kv[0], tuple(kv[1])) if isinstance(kv[1], list) else kv,
labels.items(),
)
)
)
class BoundedList(Sequence):
"""An append only list with a fixed max size.
Calls to `append` and `extend` will drop the oldest elements if there is
not enough room.
"""
def __init__(self, maxlen: Optional[int]):
self.dropped = 0
self._dq = deque(maxlen=maxlen) # type: deque
self._lock = threading.Lock()
def __repr__(self):
return f"{type(self).__name__}({list(self._dq)}, maxlen={self._dq.maxlen})"
def __getitem__(self, index):
return self._dq[index]
def __len__(self):
return len(self._dq)
def __iter__(self):
with self._lock:
return iter(deque(self._dq))
def append(self, item):
with self._lock:
if self._dq.maxlen is not None and len(self._dq) == self._dq.maxlen:
self.dropped += 1
self._dq.append(item)
def extend(self, seq):
with self._lock:
if self._dq.maxlen is not None:
to_drop = len(seq) + len(self._dq) - self._dq.maxlen
if to_drop > 0:
self.dropped += to_drop
self._dq.extend(seq)
@classmethod
def from_seq(cls, maxlen, seq):
seq = tuple(seq)
bounded_list = cls(maxlen)
bounded_list.extend(seq)
return bounded_list
@deprecated(version="1.4.0") # type: ignore
class BoundedDict(MutableMapping):
"""An ordered dict with a fixed max capacity.
Oldest elements are dropped when the dict is full and a new element is
added.
"""
def __init__(self, maxlen: Optional[int]):
if maxlen is not None:
if not isinstance(maxlen, int):
raise ValueError
if maxlen < 0:
raise ValueError
self.maxlen = maxlen
self.dropped = 0
self._dict = OrderedDict() # type: OrderedDict
self._lock = threading.Lock() # type: threading.Lock
def __repr__(self):
return f"{type(self).__name__}({dict(self._dict)}, maxlen={self.maxlen})"
def __getitem__(self, key):
return self._dict[key]
def __setitem__(self, key, value):
with self._lock:
if self.maxlen is not None and self.maxlen == 0:
self.dropped += 1
return
if key in self._dict:
del self._dict[key]
elif self.maxlen is not None and len(self._dict) == self.maxlen:
del self._dict[next(iter(self._dict.keys()))]
self.dropped += 1
self._dict[key] = value
def __delitem__(self, key):
del self._dict[key]
def __iter__(self):
with self._lock:
return iter(self._dict.copy())
def __len__(self):
return len(self._dict)
@classmethod
def from_map(cls, maxlen, mapping):
mapping = OrderedDict(mapping)
bounded_dict = cls(maxlen)
for key, value in mapping.items():
bounded_dict[key] = value
return bounded_dict
@@ -0,0 +1,147 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from json import dumps
from typing import Optional
from deprecated import deprecated
class InstrumentationInfo:
"""Immutable information about an instrumentation library module.
See `opentelemetry.trace.TracerProvider.get_tracer` for the meaning of these
properties.
"""
__slots__ = ("_name", "_version", "_schema_url")
@deprecated(version="1.11.1", reason="You should use InstrumentationScope")
def __init__(
self,
name: str,
version: Optional[str] = None,
schema_url: Optional[str] = None,
):
self._name = name
self._version = version
if schema_url is None:
schema_url = ""
self._schema_url = schema_url
def __repr__(self):
return (
f"{type(self).__name__}({self._name}, {self._version}, {self._schema_url})"
)
def __hash__(self):
return hash((self._name, self._version, self._schema_url))
def __eq__(self, value):
return type(value) is type(self) and (
self._name,
self._version,
self._schema_url,
) == (value._name, value._version, value._schema_url)
def __lt__(self, value):
if type(value) is not type(self):
return NotImplemented
return (self._name, self._version, self._schema_url) < (
value._name,
value._version,
value._schema_url,
)
@property
def schema_url(self) -> Optional[str]:
return self._schema_url
@property
def version(self) -> Optional[str]:
return self._version
@property
def name(self) -> str:
return self._name
class InstrumentationScope:
"""A logical unit of the application code with which the emitted telemetry can be
associated.
See `opentelemetry.trace.TracerProvider.get_tracer` for the meaning of these
properties.
"""
__slots__ = ("_name", "_version", "_schema_url")
def __init__(
self,
name: str,
version: Optional[str] = None,
schema_url: Optional[str] = None,
) -> None:
self._name = name
self._version = version
if schema_url is None:
schema_url = ""
self._schema_url = schema_url
def __repr__(self) -> str:
return (
f"{type(self).__name__}({self._name}, {self._version}, {self._schema_url})"
)
def __hash__(self) -> int:
return hash((self._name, self._version, self._schema_url))
def __eq__(self, value: object) -> bool:
if not isinstance(value, InstrumentationScope):
return NotImplemented
return (self._name, self._version, self._schema_url) == (
value._name,
value._version,
value._schema_url,
)
def __lt__(self, value: object) -> bool:
if not isinstance(value, InstrumentationScope):
return NotImplemented
return (self._name, self._version, self._schema_url) < (
value._name,
value._version,
value._schema_url,
)
@property
def schema_url(self) -> Optional[str]:
return self._schema_url
@property
def version(self) -> Optional[str]:
return self._version
@property
def name(self) -> str:
return self._name
def to_json(self, indent=4) -> str:
return dumps(
{
"name": self._name,
"version": self._version,
"schema_url": self._schema_url,
},
indent=indent,
)
+15
View File
@@ -0,0 +1,15 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
__version__ = "1.18.0"
@@ -0,0 +1,33 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# pylint: disable=too-many-lines
class MetricInstruments:
HTTP_SERVER_DURATION = "http.server.duration"
HTTP_SERVER_REQUEST_SIZE = "http.server.request.size"
HTTP_SERVER_RESPONSE_SIZE = "http.server.response.size"
HTTP_SERVER_ACTIVE_REQUESTS = "http.server.active_requests"
HTTP_CLIENT_DURATION = "http.client.duration"
HTTP_CLIENT_REQUEST_SIZE = "http.client.request.size"
HTTP_CLIENT_RESPONSE_SIZE = "http.client.response.size"
DB_CLIENT_CONNECTIONS_USAGE = "db.client.connections.usage"
@@ -0,0 +1,657 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from enum import Enum
class ResourceAttributes:
CLOUD_PROVIDER = "cloud.provider"
"""
Name of the cloud provider.
"""
CLOUD_ACCOUNT_ID = "cloud.account.id"
"""
The cloud account ID the resource is assigned to.
"""
CLOUD_REGION = "cloud.region"
"""
The geographical region the resource is running.
Note: Refer to your provider's docs to see the available regions, for example [Alibaba Cloud regions](https://www.alibabacloud.com/help/doc-detail/40654.htm), [AWS regions](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/), [Azure regions](https://azure.microsoft.com/en-us/global-infrastructure/geographies/), [Google Cloud regions](https://cloud.google.com/about/locations), or [Tencent Cloud regions](https://intl.cloud.tencent.com/document/product/213/6091).
"""
CLOUD_AVAILABILITY_ZONE = "cloud.availability_zone"
"""
Cloud regions often have multiple, isolated locations known as zones to increase availability. Availability zone represents the zone where the resource is running.
Note: Availability zones are called "zones" on Alibaba Cloud and Google Cloud.
"""
CLOUD_PLATFORM = "cloud.platform"
"""
The cloud platform in use.
Note: The prefix of the service SHOULD match the one specified in `cloud.provider`.
"""
AWS_ECS_CONTAINER_ARN = "aws.ecs.container.arn"
"""
The Amazon Resource Name (ARN) of an [ECS container instance](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ECS_instances.html).
"""
AWS_ECS_CLUSTER_ARN = "aws.ecs.cluster.arn"
"""
The ARN of an [ECS cluster](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/clusters.html).
"""
AWS_ECS_LAUNCHTYPE = "aws.ecs.launchtype"
"""
The [launch type](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/launch_types.html) for an ECS task.
"""
AWS_ECS_TASK_ARN = "aws.ecs.task.arn"
"""
The ARN of an [ECS task definition](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definitions.html).
"""
AWS_ECS_TASK_FAMILY = "aws.ecs.task.family"
"""
The task definition family this task definition is a member of.
"""
AWS_ECS_TASK_REVISION = "aws.ecs.task.revision"
"""
The revision for this task definition.
"""
AWS_EKS_CLUSTER_ARN = "aws.eks.cluster.arn"
"""
The ARN of an EKS cluster.
"""
AWS_LOG_GROUP_NAMES = "aws.log.group.names"
"""
The name(s) of the AWS log group(s) an application is writing to.
Note: Multiple log groups must be supported for cases like multi-container applications, where a single application has sidecar containers, and each write to their own log group.
"""
AWS_LOG_GROUP_ARNS = "aws.log.group.arns"
"""
The Amazon Resource Name(s) (ARN) of the AWS log group(s).
Note: See the [log group ARN format documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/iam-access-control-overview-cwl.html#CWL_ARN_Format).
"""
AWS_LOG_STREAM_NAMES = "aws.log.stream.names"
"""
The name(s) of the AWS log stream(s) an application is writing to.
"""
AWS_LOG_STREAM_ARNS = "aws.log.stream.arns"
"""
The ARN(s) of the AWS log stream(s).
Note: See the [log stream ARN format documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/iam-access-control-overview-cwl.html#CWL_ARN_Format). One log group can contain several log streams, so these ARNs necessarily identify both a log group and a log stream.
"""
CONTAINER_NAME = "container.name"
"""
Container name used by container runtime.
"""
CONTAINER_ID = "container.id"
"""
Container ID. Usually a UUID, as for example used to [identify Docker containers](https://docs.docker.com/engine/reference/run/#container-identification). The UUID might be abbreviated.
"""
CONTAINER_RUNTIME = "container.runtime"
"""
The container runtime managing this container.
"""
CONTAINER_IMAGE_NAME = "container.image.name"
"""
Name of the image the container was built on.
"""
CONTAINER_IMAGE_TAG = "container.image.tag"
"""
Container image tag.
"""
DEPLOYMENT_ENVIRONMENT = "deployment.environment"
"""
Name of the [deployment environment](https://en.wikipedia.org/wiki/Deployment_environment) (aka deployment tier).
"""
DEVICE_ID = "device.id"
"""
A unique identifier representing the device.
Note: The device identifier MUST only be defined using the values outlined below. This value is not an advertising identifier and MUST NOT be used as such. On iOS (Swift or Objective-C), this value MUST be equal to the [vendor identifier](https://developer.apple.com/documentation/uikit/uidevice/1620059-identifierforvendor). On Android (Java or Kotlin), this value MUST be equal to the Firebase Installation ID or a globally unique UUID which is persisted across sessions in your application. More information can be found [here](https://developer.android.com/training/articles/user-data-ids) on best practices and exact implementation details. Caution should be taken when storing personal data or anything which can identify a user. GDPR and data protection laws may apply, ensure you do your own due diligence.
"""
DEVICE_MODEL_IDENTIFIER = "device.model.identifier"
"""
The model identifier for the device.
Note: It's recommended this value represents a machine readable version of the model identifier rather than the market or consumer-friendly name of the device.
"""
DEVICE_MODEL_NAME = "device.model.name"
"""
The marketing name for the device model.
Note: It's recommended this value represents a human readable version of the device model rather than a machine readable alternative.
"""
DEVICE_MANUFACTURER = "device.manufacturer"
"""
The name of the device manufacturer.
Note: The Android OS provides this field via [Build](https://developer.android.com/reference/android/os/Build#MANUFACTURER). iOS apps SHOULD hardcode the value `Apple`.
"""
FAAS_NAME = "faas.name"
"""
The name of the single function that this runtime instance executes.
Note: This is the name of the function as configured/deployed on the FaaS platform and is usually different from the name of the callback function (which may be stored in the [`code.namespace`/`code.function`](../../trace/semantic_conventions/span-general.md#source-code-attributes) span attributes).
"""
FAAS_ID = "faas.id"
"""
The unique ID of the single function that this runtime instance executes.
Note: Depending on the cloud provider, use:
* **AWS Lambda:** The function [ARN](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html).
Take care not to use the "invoked ARN" directly but replace any
[alias suffix](https://docs.aws.amazon.com/lambda/latest/dg/configuration-aliases.html) with the resolved function version, as the same runtime instance may be invocable with multiple
different aliases.
* **GCP:** The [URI of the resource](https://cloud.google.com/iam/docs/full-resource-names)
* **Azure:** The [Fully Qualified Resource ID](https://docs.microsoft.com/en-us/rest/api/resources/resources/get-by-id).
On some providers, it may not be possible to determine the full ID at startup,
which is why this field cannot be made required. For example, on AWS the account ID
part of the ARN is not available without calling another AWS API
which may be deemed too slow for a short-running lambda function.
As an alternative, consider setting `faas.id` as a span attribute instead.
"""
FAAS_VERSION = "faas.version"
"""
The immutable version of the function being executed.
Note: Depending on the cloud provider and platform, use:
* **AWS Lambda:** The [function version](https://docs.aws.amazon.com/lambda/latest/dg/configuration-versions.html)
(an integer represented as a decimal string).
* **Google Cloud Run:** The [revision](https://cloud.google.com/run/docs/managing/revisions)
(i.e., the function name plus the revision suffix).
* **Google Cloud Functions:** The value of the
[`K_REVISION` environment variable](https://cloud.google.com/functions/docs/env-var#runtime_environment_variables_set_automatically).
* **Azure Functions:** Not applicable. Do not set this attribute.
"""
FAAS_INSTANCE = "faas.instance"
"""
The execution environment ID as a string, that will be potentially reused for other invocations to the same function/function version.
Note: * **AWS Lambda:** Use the (full) log stream name.
"""
FAAS_MAX_MEMORY = "faas.max_memory"
"""
The amount of memory available to the serverless function in MiB.
Note: It's recommended to set this attribute since e.g. too little memory can easily stop a Java AWS Lambda function from working correctly. On AWS Lambda, the environment variable `AWS_LAMBDA_FUNCTION_MEMORY_SIZE` provides this information.
"""
HOST_ID = "host.id"
"""
Unique host ID. For Cloud, this must be the instance_id assigned by the cloud provider.
"""
HOST_NAME = "host.name"
"""
Name of the host. On Unix systems, it may contain what the hostname command returns, or the fully qualified hostname, or another name specified by the user.
"""
HOST_TYPE = "host.type"
"""
Type of host. For Cloud, this must be the machine type.
"""
HOST_ARCH = "host.arch"
"""
The CPU architecture the host system is running on.
"""
HOST_IMAGE_NAME = "host.image.name"
"""
Name of the VM image or OS install the host was instantiated from.
"""
HOST_IMAGE_ID = "host.image.id"
"""
VM image ID. For Cloud, this value is from the provider.
"""
HOST_IMAGE_VERSION = "host.image.version"
"""
The version string of the VM image as defined in [Version Attributes](README.md#version-attributes).
"""
K8S_CLUSTER_NAME = "k8s.cluster.name"
"""
The name of the cluster.
"""
K8S_NODE_NAME = "k8s.node.name"
"""
The name of the Node.
"""
K8S_NODE_UID = "k8s.node.uid"
"""
The UID of the Node.
"""
K8S_NAMESPACE_NAME = "k8s.namespace.name"
"""
The name of the namespace that the pod is running in.
"""
K8S_POD_UID = "k8s.pod.uid"
"""
The UID of the Pod.
"""
K8S_POD_NAME = "k8s.pod.name"
"""
The name of the Pod.
"""
K8S_CONTAINER_NAME = "k8s.container.name"
"""
The name of the Container from Pod specification, must be unique within a Pod. Container runtime usually uses different globally unique name (`container.name`).
"""
K8S_CONTAINER_RESTART_COUNT = "k8s.container.restart_count"
"""
Number of times the container was restarted. This attribute can be used to identify a particular container (running or stopped) within a container spec.
"""
K8S_REPLICASET_UID = "k8s.replicaset.uid"
"""
The UID of the ReplicaSet.
"""
K8S_REPLICASET_NAME = "k8s.replicaset.name"
"""
The name of the ReplicaSet.
"""
K8S_DEPLOYMENT_UID = "k8s.deployment.uid"
"""
The UID of the Deployment.
"""
K8S_DEPLOYMENT_NAME = "k8s.deployment.name"
"""
The name of the Deployment.
"""
K8S_STATEFULSET_UID = "k8s.statefulset.uid"
"""
The UID of the StatefulSet.
"""
K8S_STATEFULSET_NAME = "k8s.statefulset.name"
"""
The name of the StatefulSet.
"""
K8S_DAEMONSET_UID = "k8s.daemonset.uid"
"""
The UID of the DaemonSet.
"""
K8S_DAEMONSET_NAME = "k8s.daemonset.name"
"""
The name of the DaemonSet.
"""
K8S_JOB_UID = "k8s.job.uid"
"""
The UID of the Job.
"""
K8S_JOB_NAME = "k8s.job.name"
"""
The name of the Job.
"""
K8S_CRONJOB_UID = "k8s.cronjob.uid"
"""
The UID of the CronJob.
"""
K8S_CRONJOB_NAME = "k8s.cronjob.name"
"""
The name of the CronJob.
"""
OS_TYPE = "os.type"
"""
The operating system type.
"""
OS_DESCRIPTION = "os.description"
"""
Human readable (not intended to be parsed) OS version information, like e.g. reported by `ver` or `lsb_release -a` commands.
"""
OS_NAME = "os.name"
"""
Human readable operating system name.
"""
OS_VERSION = "os.version"
"""
The version string of the operating system as defined in [Version Attributes](../../resource/semantic_conventions/README.md#version-attributes).
"""
PROCESS_PID = "process.pid"
"""
Process identifier (PID).
"""
PROCESS_EXECUTABLE_NAME = "process.executable.name"
"""
The name of the process executable. On Linux based systems, can be set to the `Name` in `proc/[pid]/status`. On Windows, can be set to the base name of `GetProcessImageFileNameW`.
"""
PROCESS_EXECUTABLE_PATH = "process.executable.path"
"""
The full path to the process executable. On Linux based systems, can be set to the target of `proc/[pid]/exe`. On Windows, can be set to the result of `GetProcessImageFileNameW`.
"""
PROCESS_COMMAND = "process.command"
"""
The command used to launch the process (i.e. the command name). On Linux based systems, can be set to the zeroth string in `proc/[pid]/cmdline`. On Windows, can be set to the first parameter extracted from `GetCommandLineW`.
"""
PROCESS_COMMAND_LINE = "process.command_line"
"""
The full command used to launch the process as a single string representing the full command. On Windows, can be set to the result of `GetCommandLineW`. Do not set this if you have to assemble it just for monitoring; use `process.command_args` instead.
"""
PROCESS_COMMAND_ARGS = "process.command_args"
"""
All the command arguments (including the command/executable itself) as received by the process. On Linux-based systems (and some other Unixoid systems supporting procfs), can be set according to the list of null-delimited strings extracted from `proc/[pid]/cmdline`. For libc-based executables, this would be the full argv vector passed to `main`.
"""
PROCESS_OWNER = "process.owner"
"""
The username of the user that owns the process.
"""
PROCESS_RUNTIME_NAME = "process.runtime.name"
"""
The name of the runtime of this process. For compiled native binaries, this SHOULD be the name of the compiler.
"""
PROCESS_RUNTIME_VERSION = "process.runtime.version"
"""
The version of the runtime of this process, as returned by the runtime without modification.
"""
PROCESS_RUNTIME_DESCRIPTION = "process.runtime.description"
"""
An additional description about the runtime of the process, for example a specific vendor customization of the runtime environment.
"""
SERVICE_NAME = "service.name"
"""
Logical name of the service.
Note: MUST be the same for all instances of horizontally scaled services. If the value was not specified, SDKs MUST fallback to `unknown_service:` concatenated with [`process.executable.name`](process.md#process), e.g. `unknown_service:bash`. If `process.executable.name` is not available, the value MUST be set to `unknown_service`.
"""
SERVICE_NAMESPACE = "service.namespace"
"""
A namespace for `service.name`.
Note: A string value having a meaning that helps to distinguish a group of services, for example the team name that owns a group of services. `service.name` is expected to be unique within the same namespace. If `service.namespace` is not specified in the Resource then `service.name` is expected to be unique for all services that have no explicit namespace defined (so the empty/unspecified namespace is simply one more valid namespace). Zero-length namespace string is assumed equal to unspecified namespace.
"""
SERVICE_INSTANCE_ID = "service.instance.id"
"""
The string ID of the service instance.
Note: MUST be unique for each instance of the same `service.namespace,service.name` pair (in other words `service.namespace,service.name,service.instance.id` triplet MUST be globally unique). The ID helps to distinguish instances of the same service that exist at the same time (e.g. instances of a horizontally scaled service). It is preferable for the ID to be persistent and stay the same for the lifetime of the service instance, however it is acceptable that the ID is ephemeral and changes during important lifetime events for the service (e.g. service restarts). If the service has no inherent unique ID that can be used as the value of this attribute it is recommended to generate a random Version 1 or Version 4 RFC 4122 UUID (services aiming for reproducible UUIDs may also use Version 5, see RFC 4122 for more recommendations).
"""
SERVICE_VERSION = "service.version"
"""
The version string of the service API or implementation.
"""
TELEMETRY_SDK_NAME = "telemetry.sdk.name"
"""
The name of the telemetry SDK as defined above.
"""
TELEMETRY_SDK_LANGUAGE = "telemetry.sdk.language"
"""
The language of the telemetry SDK.
"""
TELEMETRY_SDK_VERSION = "telemetry.sdk.version"
"""
The version string of the telemetry SDK.
"""
TELEMETRY_AUTO_VERSION = "telemetry.auto.version"
"""
The version string of the auto instrumentation agent, if used.
"""
WEBENGINE_NAME = "webengine.name"
"""
The name of the web engine.
"""
WEBENGINE_VERSION = "webengine.version"
"""
The version of the web engine.
"""
WEBENGINE_DESCRIPTION = "webengine.description"
"""
Additional description of the web engine (e.g. detailed version and edition information).
"""
class CloudProviderValues(Enum):
ALIBABA_CLOUD = "alibaba_cloud"
"""Alibaba Cloud."""
AWS = "aws"
"""Amazon Web Services."""
AZURE = "azure"
"""Microsoft Azure."""
GCP = "gcp"
"""Google Cloud Platform."""
TENCENT_CLOUD = "tencent_cloud"
"""Tencent Cloud."""
class CloudPlatformValues(Enum):
ALIBABA_CLOUD_ECS = "alibaba_cloud_ecs"
"""Alibaba Cloud Elastic Compute Service."""
ALIBABA_CLOUD_FC = "alibaba_cloud_fc"
"""Alibaba Cloud Function Compute."""
AWS_EC2 = "aws_ec2"
"""AWS Elastic Compute Cloud."""
AWS_ECS = "aws_ecs"
"""AWS Elastic Container Service."""
AWS_EKS = "aws_eks"
"""AWS Elastic Kubernetes Service."""
AWS_LAMBDA = "aws_lambda"
"""AWS Lambda."""
AWS_ELASTIC_BEANSTALK = "aws_elastic_beanstalk"
"""AWS Elastic Beanstalk."""
AWS_APP_RUNNER = "aws_app_runner"
"""AWS App Runner."""
AZURE_VM = "azure_vm"
"""Azure Virtual Machines."""
AZURE_CONTAINER_INSTANCES = "azure_container_instances"
"""Azure Container Instances."""
AZURE_AKS = "azure_aks"
"""Azure Kubernetes Service."""
AZURE_FUNCTIONS = "azure_functions"
"""Azure Functions."""
AZURE_APP_SERVICE = "azure_app_service"
"""Azure App Service."""
GCP_COMPUTE_ENGINE = "gcp_compute_engine"
"""Google Cloud Compute Engine (GCE)."""
GCP_CLOUD_RUN = "gcp_cloud_run"
"""Google Cloud Run."""
GCP_KUBERNETES_ENGINE = "gcp_kubernetes_engine"
"""Google Cloud Kubernetes Engine (GKE)."""
GCP_CLOUD_FUNCTIONS = "gcp_cloud_functions"
"""Google Cloud Functions (GCF)."""
GCP_APP_ENGINE = "gcp_app_engine"
"""Google Cloud App Engine (GAE)."""
TENCENT_CLOUD_CVM = "tencent_cloud_cvm"
"""Tencent Cloud Cloud Virtual Machine (CVM)."""
TENCENT_CLOUD_EKS = "tencent_cloud_eks"
"""Tencent Cloud Elastic Kubernetes Service (EKS)."""
TENCENT_CLOUD_SCF = "tencent_cloud_scf"
"""Tencent Cloud Serverless Cloud Function (SCF)."""
class AwsEcsLaunchtypeValues(Enum):
EC2 = "ec2"
"""ec2."""
FARGATE = "fargate"
"""fargate."""
class HostArchValues(Enum):
AMD64 = "amd64"
"""AMD64."""
ARM32 = "arm32"
"""ARM32."""
ARM64 = "arm64"
"""ARM64."""
IA64 = "ia64"
"""Itanium."""
PPC32 = "ppc32"
"""32-bit PowerPC."""
PPC64 = "ppc64"
"""64-bit PowerPC."""
S390X = "s390x"
"""IBM z/Architecture."""
X86 = "x86"
"""32-bit x86."""
class OsTypeValues(Enum):
WINDOWS = "windows"
"""Microsoft Windows."""
LINUX = "linux"
"""Linux."""
DARWIN = "darwin"
"""Apple Darwin."""
FREEBSD = "freebsd"
"""FreeBSD."""
NETBSD = "netbsd"
"""NetBSD."""
OPENBSD = "openbsd"
"""OpenBSD."""
DRAGONFLYBSD = "dragonflybsd"
"""DragonFly BSD."""
HPUX = "hpux"
"""HP-UX (Hewlett Packard Unix)."""
AIX = "aix"
"""AIX (Advanced Interactive eXecutive)."""
SOLARIS = "solaris"
"""SunOS, Oracle Solaris."""
Z_OS = "z_os"
"""IBM z/OS."""
class TelemetrySdkLanguageValues(Enum):
CPP = "cpp"
"""cpp."""
DOTNET = "dotnet"
"""dotnet."""
ERLANG = "erlang"
"""erlang."""
GO = "go"
"""go."""
JAVA = "java"
"""java."""
NODEJS = "nodejs"
"""nodejs."""
PHP = "php"
"""php."""
PYTHON = "python"
"""python."""
RUBY = "ruby"
"""ruby."""
WEBJS = "webjs"
"""webjs."""
SWIFT = "swift"
"""swift."""
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
__version__ = "0.39b0"
+623
View File
@@ -0,0 +1,623 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
The OpenTelemetry tracing API describes the classes used to generate
distributed traces.
The :class:`.Tracer` class controls access to the execution context, and
manages span creation. Each operation in a trace is represented by a
:class:`.Span`, which records the start, end time, and metadata associated with
the operation.
This module provides abstract (i.e. unimplemented) classes required for
tracing, and a concrete no-op :class:`.NonRecordingSpan` that allows applications
to use the API package alone without a supporting implementation.
To get a tracer, you need to provide the package name from which you are
calling the tracer APIs to OpenTelemetry by calling `TracerProvider.get_tracer`
with the calling module name and the version of your package.
The tracer supports creating spans that are "attached" or "detached" from the
context. New spans are "attached" to the context in that they are
created as children of the currently active span, and the newly-created span
can optionally become the new active span::
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
# Create a new root span, set it as the current span in context
with tracer.start_as_current_span("parent"):
# Attach a new child and update the current span
with tracer.start_as_current_span("child"):
do_work():
# Close child span, set parent as current
# Close parent span, set default span as current
When creating a span that's "detached" from the context the active span doesn't
change, and the caller is responsible for managing the span's lifetime::
# Explicit parent span assignment is done via the Context
from mysql.opentelemetry.trace import set_span_in_context
context = set_span_in_context(parent)
child = tracer.start_span("child", context=context)
try:
do_work(span=child)
finally:
child.end()
Applications should generally use a single global TracerProvider, and use
either implicit or explicit context propagation consistently throughout.
.. versionadded:: 0.1.0
.. versionchanged:: 0.3.0
`TracerProvider` was introduced and the global ``tracer`` getter was
replaced by ``tracer_provider``.
.. versionchanged:: 0.5.0
``tracer_provider`` was replaced by `get_tracer_provider`,
``set_preferred_tracer_provider_implementation`` was replaced by
`set_tracer_provider`.
"""
import os
import typing
from abc import ABC, abstractmethod
from contextlib import contextmanager
from enum import Enum
from logging import getLogger
from typing import Iterator, Optional, Sequence, cast
from deprecated import deprecated
from mysql.opentelemetry import context as context_api
from mysql.opentelemetry.attributes import BoundedAttributes # type: ignore
from mysql.opentelemetry.context.context import Context
from mysql.opentelemetry.environment_variables import OTEL_PYTHON_TRACER_PROVIDER
from mysql.opentelemetry.trace.propagation import (
_SPAN_KEY,
get_current_span,
set_span_in_context,
)
from mysql.opentelemetry.trace.span import (
DEFAULT_TRACE_OPTIONS,
DEFAULT_TRACE_STATE,
INVALID_SPAN,
INVALID_SPAN_CONTEXT,
INVALID_SPAN_ID,
INVALID_TRACE_ID,
NonRecordingSpan,
Span,
SpanContext,
TraceFlags,
TraceState,
format_span_id,
format_trace_id,
)
from mysql.opentelemetry.trace.status import Status, StatusCode
from mysql.opentelemetry.util import types
from mysql.opentelemetry.util._once import Once
from mysql.opentelemetry.util._providers import _load_provider
logger = getLogger(__name__)
class _LinkBase(ABC):
def __init__(self, context: "SpanContext") -> None:
self._context = context
@property
def context(self) -> "SpanContext":
return self._context
@property
@abstractmethod
def attributes(self) -> types.Attributes:
pass
class Link(_LinkBase):
"""A link to a `Span`. The attributes of a Link are immutable.
Args:
context: `SpanContext` of the `Span` to link to.
attributes: Link's attributes.
"""
def __init__(
self,
context: "SpanContext",
attributes: types.Attributes = None,
) -> None:
super().__init__(context)
self._attributes = BoundedAttributes(
attributes=attributes
) # type: types.Attributes
@property
def attributes(self) -> types.Attributes:
return self._attributes
_Links = Optional[Sequence[Link]]
class SpanKind(Enum):
"""Specifies additional details on how this span relates to its parent span.
Note that this enumeration is experimental and likely to change. See
https://github.com/open-telemetry/opentelemetry-specification/pull/226.
"""
#: Default value. Indicates that the span is used internally in the
# application.
INTERNAL = 0
#: Indicates that the span describes an operation that handles a remote
# request.
SERVER = 1
#: Indicates that the span describes a request to some remote service.
CLIENT = 2
#: Indicates that the span describes a producer sending a message to a
#: broker. Unlike client and server, there is usually no direct critical
#: path latency relationship between producer and consumer spans.
PRODUCER = 3
#: Indicates that the span describes a consumer receiving a message from a
#: broker. Unlike client and server, there is usually no direct critical
#: path latency relationship between producer and consumer spans.
CONSUMER = 4
class TracerProvider(ABC):
@abstractmethod
def get_tracer(
self,
instrumenting_module_name: str,
instrumenting_library_version: typing.Optional[str] = None,
schema_url: typing.Optional[str] = None,
) -> "Tracer":
"""Returns a `Tracer` for use by the given instrumentation library.
For any two calls it is undefined whether the same or different
`Tracer` instances are returned, even for different library names.
This function may return different `Tracer` types (e.g. a no-op tracer
vs. a functional tracer).
Args:
instrumenting_module_name: The uniquely identifiable name for instrumentation
scope, such as instrumentation library, package, module or class name.
``__name__`` may not be used as this can result in
different tracer names if the tracers are in different files.
It is better to use a fixed string that can be imported where
needed and used consistently as the name of the tracer.
This should *not* be the name of the module that is
instrumented but the name of the module doing the instrumentation.
E.g., instead of ``"requests"``, use
``"mysql.opentelemetry.instrumentation.requests"``.
instrumenting_library_version: Optional. The version string of the
instrumenting library. Usually this should be the same as
``importlib.metadata.version(instrumenting_library_name)``.
schema_url: Optional. Specifies the Schema URL of the emitted telemetry.
"""
class NoOpTracerProvider(TracerProvider):
"""The default TracerProvider, used when no implementation is available.
All operations are no-op.
"""
def get_tracer(
self,
instrumenting_module_name: str,
instrumenting_library_version: typing.Optional[str] = None,
schema_url: typing.Optional[str] = None,
) -> "Tracer":
# pylint:disable=no-self-use,unused-argument
return NoOpTracer()
@deprecated(version="1.9.0", reason="You should use NoOpTracerProvider") # type: ignore
class _DefaultTracerProvider(NoOpTracerProvider):
"""The default TracerProvider, used when no implementation is available.
All operations are no-op.
"""
class ProxyTracerProvider(TracerProvider):
def get_tracer(
self,
instrumenting_module_name: str,
instrumenting_library_version: typing.Optional[str] = None,
schema_url: typing.Optional[str] = None,
) -> "Tracer":
if _TRACER_PROVIDER:
return _TRACER_PROVIDER.get_tracer(
instrumenting_module_name,
instrumenting_library_version,
schema_url,
)
return ProxyTracer(
instrumenting_module_name,
instrumenting_library_version,
schema_url,
)
class Tracer(ABC):
"""Handles span creation and in-process context propagation.
This class provides methods for manipulating the context, creating spans,
and controlling spans' lifecycles.
"""
@abstractmethod
def start_span(
self,
name: str,
context: Optional[Context] = None,
kind: SpanKind = SpanKind.INTERNAL,
attributes: types.Attributes = None,
links: _Links = None,
start_time: Optional[int] = None,
record_exception: bool = True,
set_status_on_exception: bool = True,
) -> "Span":
"""Starts a span.
Create a new span. Start the span without setting it as the current
span in the context. To start the span and use the context in a single
method, see :meth:`start_as_current_span`.
By default the current span in the context will be used as parent, but an
explicit context can also be specified, by passing in a `Context` containing
a current `Span`. If there is no current span in the global `Context` or in
the specified context, the created span will be a root span.
The span can be used as a context manager. On exiting the context manager,
the span's end() method will be called.
Example::
# trace.get_current_span() will be used as the implicit parent.
# If none is found, the created span will be a root instance.
with tracer.start_span("one") as child:
child.add_event("child's event")
Args:
name: The name of the span to be created.
context: An optional Context containing the span's parent. Defaults to the
global context.
kind: The span's kind (relationship to parent). Note that is
meaningful even if there is no parent.
attributes: The span's attributes.
links: Links span to other spans
start_time: Sets the start time of a span
record_exception: Whether to record any exceptions raised within the
context as error event on the span.
set_status_on_exception: Only relevant if the returned span is used
in a with/context manager. Defines whether the span status will
be automatically set to ERROR when an uncaught exception is
raised in the span with block. The span status won't be set by
this mechanism if it was previously set manually.
Returns:
The newly-created span.
"""
@contextmanager
@abstractmethod
def start_as_current_span(
self,
name: str,
context: Optional[Context] = None,
kind: SpanKind = SpanKind.INTERNAL,
attributes: types.Attributes = None,
links: _Links = None,
start_time: Optional[int] = None,
record_exception: bool = True,
set_status_on_exception: bool = True,
end_on_exit: bool = True,
) -> Iterator["Span"]:
"""Context manager for creating a new span and set it
as the current span in this tracer's context.
Exiting the context manager will call the span's end method,
as well as return the current span to its previous value by
returning to the previous context.
Example::
with tracer.start_as_current_span("one") as parent:
parent.add_event("parent's event")
with tracer.start_as_current_span("two") as child:
child.add_event("child's event")
trace.get_current_span() # returns child
trace.get_current_span() # returns parent
trace.get_current_span() # returns previously active span
This is a convenience method for creating spans attached to the
tracer's context. Applications that need more control over the span
lifetime should use :meth:`start_span` instead. For example::
with tracer.start_as_current_span(name) as span:
do_work()
is equivalent to::
span = tracer.start_span(name)
with mysql.opentelemetry.trace.use_span(span, end_on_exit=True):
do_work()
This can also be used as a decorator::
@tracer.start_as_current_span("name")
def function():
...
function()
Args:
name: The name of the span to be created.
context: An optional Context containing the span's parent. Defaults to the
global context.
kind: The span's kind (relationship to parent). Note that is
meaningful even if there is no parent.
attributes: The span's attributes.
links: Links span to other spans
start_time: Sets the start time of a span
record_exception: Whether to record any exceptions raised within the
context as error event on the span.
set_status_on_exception: Only relevant if the returned span is used
in a with/context manager. Defines whether the span status will
be automatically set to ERROR when an uncaught exception is
raised in the span with block. The span status won't be set by
this mechanism if it was previously set manually.
end_on_exit: Whether to end the span automatically when leaving the
context manager.
Yields:
The newly-created span.
"""
class ProxyTracer(Tracer):
# pylint: disable=W0222,signature-differs
def __init__(
self,
instrumenting_module_name: str,
instrumenting_library_version: typing.Optional[str] = None,
schema_url: typing.Optional[str] = None,
):
self._instrumenting_module_name = instrumenting_module_name
self._instrumenting_library_version = instrumenting_library_version
self._schema_url = schema_url
self._real_tracer: Optional[Tracer] = None
self._noop_tracer = NoOpTracer()
@property
def _tracer(self) -> Tracer:
if self._real_tracer:
return self._real_tracer
if _TRACER_PROVIDER:
self._real_tracer = _TRACER_PROVIDER.get_tracer(
self._instrumenting_module_name,
self._instrumenting_library_version,
self._schema_url,
)
return self._real_tracer
return self._noop_tracer
def start_span(self, *args, **kwargs) -> Span: # type: ignore
return self._tracer.start_span(*args, **kwargs) # type: ignore
@contextmanager # type: ignore
def start_as_current_span(self, *args, **kwargs) -> Iterator[Span]: # type: ignore
with self._tracer.start_as_current_span(*args, **kwargs) as span: # type: ignore
yield span
class NoOpTracer(Tracer):
"""The default Tracer, used when no Tracer implementation is available.
All operations are no-op.
"""
def start_span(
self,
name: str,
context: Optional[Context] = None,
kind: SpanKind = SpanKind.INTERNAL,
attributes: types.Attributes = None,
links: _Links = None,
start_time: Optional[int] = None,
record_exception: bool = True,
set_status_on_exception: bool = True,
) -> "Span":
# pylint: disable=unused-argument,no-self-use
return INVALID_SPAN
@contextmanager
def start_as_current_span(
self,
name: str,
context: Optional[Context] = None,
kind: SpanKind = SpanKind.INTERNAL,
attributes: types.Attributes = None,
links: _Links = None,
start_time: Optional[int] = None,
record_exception: bool = True,
set_status_on_exception: bool = True,
end_on_exit: bool = True,
) -> Iterator["Span"]:
# pylint: disable=unused-argument,no-self-use
yield INVALID_SPAN
@deprecated(version="1.9.0", reason="You should use NoOpTracer") # type: ignore
class _DefaultTracer(NoOpTracer):
"""The default Tracer, used when no Tracer implementation is available.
All operations are no-op.
"""
_TRACER_PROVIDER_SET_ONCE = Once()
_TRACER_PROVIDER: Optional[TracerProvider] = None
_PROXY_TRACER_PROVIDER = ProxyTracerProvider()
def get_tracer(
instrumenting_module_name: str,
instrumenting_library_version: typing.Optional[str] = None,
tracer_provider: Optional[TracerProvider] = None,
schema_url: typing.Optional[str] = None,
) -> "Tracer":
"""Returns a `Tracer` for use by the given instrumentation library.
This function is a convenience wrapper for
mysql.opentelemetry.trace.TracerProvider.get_tracer.
If tracer_provider is omitted the current configured one is used.
"""
if tracer_provider is None:
tracer_provider = get_tracer_provider()
return tracer_provider.get_tracer(
instrumenting_module_name, instrumenting_library_version, schema_url
)
def _set_tracer_provider(tracer_provider: TracerProvider, log: bool) -> None:
def set_tp() -> None:
global _TRACER_PROVIDER # pylint: disable=global-statement
_TRACER_PROVIDER = tracer_provider
did_set = _TRACER_PROVIDER_SET_ONCE.do_once(set_tp)
if log and not did_set:
logger.warning("Overriding of current TracerProvider is not allowed")
def set_tracer_provider(tracer_provider: TracerProvider) -> None:
"""Sets the current global :class:`~.TracerProvider` object.
This can only be done once, a warning will be logged if any further attempt
is made.
"""
_set_tracer_provider(tracer_provider, log=True)
def get_tracer_provider() -> TracerProvider:
"""Gets the current global :class:`~.TracerProvider` object."""
if _TRACER_PROVIDER is None:
# if a global tracer provider has not been set either via code or env
# vars, return a proxy tracer provider
if OTEL_PYTHON_TRACER_PROVIDER not in os.environ:
return _PROXY_TRACER_PROVIDER
tracer_provider: TracerProvider = _load_provider(
OTEL_PYTHON_TRACER_PROVIDER, "tracer_provider"
)
_set_tracer_provider(tracer_provider, log=False)
# _TRACER_PROVIDER will have been set by one thread
return cast("TracerProvider", _TRACER_PROVIDER)
@contextmanager
def use_span(
span: Span,
end_on_exit: bool = False,
record_exception: bool = True,
set_status_on_exception: bool = True,
) -> Iterator[Span]:
"""Takes a non-active span and activates it in the current context.
Args:
span: The span that should be activated in the current context.
end_on_exit: Whether to end the span automatically when leaving the
context manager scope.
record_exception: Whether to record any exceptions raised within the
context as error event on the span.
set_status_on_exception: Only relevant if the returned span is used
in a with/context manager. Defines whether the span status will
be automatically set to ERROR when an uncaught exception is
raised in the span with block. The span status won't be set by
this mechanism if it was previously set manually.
"""
try:
token = context_api.attach(context_api.set_value(_SPAN_KEY, span))
try:
yield span
finally:
context_api.detach(token)
except Exception as exc: # pylint: disable=broad-except
if isinstance(span, Span) and span.is_recording():
# Record the exception as an event
if record_exception:
span.record_exception(exc)
# Set status in case exception was raised
if set_status_on_exception:
span.set_status(
Status(
status_code=StatusCode.ERROR,
description=f"{type(exc).__name__}: {exc}",
)
)
raise
finally:
if end_on_exit:
span.end()
__all__ = [
"DEFAULT_TRACE_OPTIONS",
"DEFAULT_TRACE_STATE",
"INVALID_SPAN",
"INVALID_SPAN_CONTEXT",
"INVALID_SPAN_ID",
"INVALID_TRACE_ID",
"NonRecordingSpan",
"Link",
"Span",
"SpanContext",
"SpanKind",
"TraceFlags",
"TraceState",
"TracerProvider",
"Tracer",
"format_span_id",
"format_trace_id",
"get_current_span",
"get_tracer",
"get_tracer_provider",
"set_tracer_provider",
"set_span_in_context",
"use_span",
"Status",
"StatusCode",
]
@@ -0,0 +1,49 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Optional
from mysql.opentelemetry.context import create_key, get_value, set_value
from mysql.opentelemetry.context.context import Context
from mysql.opentelemetry.trace.span import INVALID_SPAN, Span
SPAN_KEY = "current-span"
_SPAN_KEY = create_key("current-span")
def set_span_in_context(span: Span, context: Optional[Context] = None) -> Context:
"""Set the span in the given context.
Args:
span: The Span to set.
context: a Context object. if one is not passed, the
default current context is used instead.
"""
ctx = set_value(_SPAN_KEY, span, context=context)
return ctx
def get_current_span(context: Optional[Context] = None) -> Span:
"""Retrieve the current span.
Args:
context: A Context object. If one is not passed, the
default current context is used instead.
Returns:
The Span set in the context if it exists. INVALID_SPAN otherwise.
"""
span = get_value(_SPAN_KEY, context=context)
if span is None or not isinstance(span, Span):
return INVALID_SPAN
return span
@@ -0,0 +1,114 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import re
import typing
from mysql.opentelemetry import trace
from mysql.opentelemetry.context.context import Context
from mysql.opentelemetry.propagators import textmap
from mysql.opentelemetry.trace import format_span_id, format_trace_id
from mysql.opentelemetry.trace.span import TraceState
class TraceContextTextMapPropagator(textmap.TextMapPropagator):
"""Extracts and injects using w3c TraceContext's headers."""
_TRACEPARENT_HEADER_NAME = "traceparent"
_TRACESTATE_HEADER_NAME = "tracestate"
_TRACEPARENT_HEADER_FORMAT = (
"^[ \t]*([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})"
+ "(-.*)?[ \t]*$"
)
_TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT)
def extract(
self,
carrier: textmap.CarrierT,
context: typing.Optional[Context] = None,
getter: textmap.Getter[textmap.CarrierT] = textmap.default_getter,
) -> Context:
"""Extracts SpanContext from the carrier.
See `mysql.opentelemetry.propagators.textmap.TextMapPropagator.extract`
"""
if context is None:
context = Context()
header = getter.get(carrier, self._TRACEPARENT_HEADER_NAME)
if not header:
return context
match = re.search(self._TRACEPARENT_HEADER_FORMAT_RE, header[0])
if not match:
return context
version: str = match.group(1)
trace_id: str = match.group(2)
span_id: str = match.group(3)
trace_flags: str = match.group(4)
if trace_id == "0" * 32 or span_id == "0" * 16:
return context
if version == "00":
if match.group(5): # type: ignore
return context
if version == "ff":
return context
tracestate_headers = getter.get(carrier, self._TRACESTATE_HEADER_NAME)
if tracestate_headers is None:
tracestate = None
else:
tracestate = TraceState.from_header(tracestate_headers)
span_context = trace.SpanContext(
trace_id=int(trace_id, 16),
span_id=int(span_id, 16),
is_remote=True,
trace_flags=trace.TraceFlags(int(trace_flags, 16)),
trace_state=tracestate,
)
return trace.set_span_in_context(trace.NonRecordingSpan(span_context), context)
def inject(
self,
carrier: textmap.CarrierT,
context: typing.Optional[Context] = None,
setter: textmap.Setter[textmap.CarrierT] = textmap.default_setter,
) -> None:
"""Injects SpanContext into the carrier.
See `mysql.opentelemetry.propagators.textmap.TextMapPropagator.inject`
"""
span = trace.get_current_span(context)
span_context = span.get_span_context()
if span_context == trace.INVALID_SPAN_CONTEXT:
return
traceparent_string = f"00-{format_trace_id(span_context.trace_id)}-{format_span_id(span_context.span_id)}-{span_context.trace_flags:02x}"
setter.set(carrier, self._TRACEPARENT_HEADER_NAME, traceparent_string)
if span_context.trace_state:
tracestate_string = span_context.trace_state.to_header()
setter.set(carrier, self._TRACESTATE_HEADER_NAME, tracestate_string)
@property
def fields(self) -> typing.Set[str]:
"""Returns a set with the fields set in `inject`.
See
`mysql.opentelemetry.propagators.textmap.TextMapPropagator.fields`
"""
return {self._TRACEPARENT_HEADER_NAME, self._TRACESTATE_HEADER_NAME}
+563
View File
@@ -0,0 +1,563 @@
import abc
import logging
import re
import types as python_types
import typing
from collections import OrderedDict
from mysql.opentelemetry.trace.status import Status, StatusCode
from mysql.opentelemetry.util import types
# The key MUST begin with a lowercase letter or a digit,
# and can only contain lowercase letters (a-z), digits (0-9),
# underscores (_), dashes (-), asterisks (*), and forward slashes (/).
# For multi-tenant vendor scenarios, an at sign (@) can be used to
# prefix the vendor name. Vendors SHOULD set the tenant ID
# at the beginning of the key.
# key = ( lcalpha ) 0*255( lcalpha / DIGIT / "_" / "-"/ "*" / "/" )
# key = ( lcalpha / DIGIT ) 0*240( lcalpha / DIGIT / "_" / "-"/ "*" / "/" ) "@" lcalpha 0*13( lcalpha / DIGIT / "_" / "-"/ "*" / "/" )
# lcalpha = %x61-7A ; a-z
_KEY_FORMAT = (
r"[a-z][_0-9a-z\-\*\/]{0,255}|"
r"[a-z0-9][_0-9a-z\-\*\/]{0,240}@[a-z][_0-9a-z\-\*\/]{0,13}"
)
_KEY_PATTERN = re.compile(_KEY_FORMAT)
# The value is an opaque string containing up to 256 printable
# ASCII [RFC0020] characters (i.e., the range 0x20 to 0x7E)
# except comma (,) and (=).
# value = 0*255(chr) nblk-chr
# nblk-chr = %x21-2B / %x2D-3C / %x3E-7E
# chr = %x20 / nblk-chr
_VALUE_FORMAT = r"[\x20-\x2b\x2d-\x3c\x3e-\x7e]{0,255}[\x21-\x2b\x2d-\x3c\x3e-\x7e]"
_VALUE_PATTERN = re.compile(_VALUE_FORMAT)
_TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS = 32
_delimiter_pattern = re.compile(r"[ \t]*,[ \t]*")
_member_pattern = re.compile(f"({_KEY_FORMAT})(=)({_VALUE_FORMAT})[ \t]*")
_logger = logging.getLogger(__name__)
def _is_valid_pair(key: str, value: str) -> bool:
return (
isinstance(key, str)
and _KEY_PATTERN.fullmatch(key) is not None
and isinstance(value, str)
and _VALUE_PATTERN.fullmatch(value) is not None
)
class Span(abc.ABC):
"""A span represents a single operation within a trace."""
@abc.abstractmethod
def end(self, end_time: typing.Optional[int] = None) -> None:
"""Sets the current time as the span's end time.
The span's end time is the wall time at which the operation finished.
Only the first call to `end` should modify the span, and
implementations are free to ignore or raise on further calls.
"""
@abc.abstractmethod
def get_span_context(self) -> "SpanContext":
"""Gets the span's SpanContext.
Get an immutable, serializable identifier for this span that can be
used to create new child spans.
Returns:
A :class:`opentelemetry.trace.SpanContext` with a copy of this span's immutable state.
"""
@abc.abstractmethod
def set_attributes(
self, attributes: typing.Dict[str, types.AttributeValue]
) -> None:
"""Sets Attributes.
Sets Attributes with the key and value passed as arguments dict.
Note: The behavior of `None` value attributes is undefined, and hence
strongly discouraged. It is also preferred to set attributes at span
creation, instead of calling this method later since samplers can only
consider information already present during span creation.
"""
@abc.abstractmethod
def set_attribute(self, key: str, value: types.AttributeValue) -> None:
"""Sets an Attribute.
Sets a single Attribute with the key and value passed as arguments.
Note: The behavior of `None` value attributes is undefined, and hence
strongly discouraged. It is also preferred to set attributes at span
creation, instead of calling this method later since samplers can only
consider information already present during span creation.
"""
@abc.abstractmethod
def add_event(
self,
name: str,
attributes: types.Attributes = None,
timestamp: typing.Optional[int] = None,
) -> None:
"""Adds an `Event`.
Adds a single `Event` with the name and, optionally, a timestamp and
attributes passed as arguments. Implementations should generate a
timestamp if the `timestamp` argument is omitted.
"""
@abc.abstractmethod
def update_name(self, name: str) -> None:
"""Updates the `Span` name.
This will override the name provided via :func:`opentelemetry.trace.Tracer.start_span`.
Upon this update, any sampling behavior based on Span name will depend
on the implementation.
"""
@abc.abstractmethod
def is_recording(self) -> bool:
"""Returns whether this span will be recorded.
Returns true if this Span is active and recording information like
events with the add_event operation and attributes using set_attribute.
"""
@abc.abstractmethod
def set_status(
self,
status: typing.Union[Status, StatusCode],
description: typing.Optional[str] = None,
) -> None:
"""Sets the Status of the Span. If used, this will override the default
Span status.
"""
@abc.abstractmethod
def record_exception(
self,
exception: Exception,
attributes: types.Attributes = None,
timestamp: typing.Optional[int] = None,
escaped: bool = False,
) -> None:
"""Records an exception as a span event."""
def __enter__(self) -> "Span":
"""Invoked when `Span` is used as a context manager.
Returns the `Span` itself.
"""
return self
def __exit__(
self,
exc_type: typing.Optional[typing.Type[BaseException]],
exc_val: typing.Optional[BaseException],
exc_tb: typing.Optional[python_types.TracebackType],
) -> None:
"""Ends context manager and calls `end` on the `Span`."""
self.end()
class TraceFlags(int):
"""A bitmask that represents options specific to the trace.
The only supported option is the "sampled" flag (``0x01``). If set, this
flag indicates that the trace may have been sampled upstream.
See the `W3C Trace Context - Traceparent`_ spec for details.
.. _W3C Trace Context - Traceparent:
https://www.w3.org/TR/trace-context/#trace-flags
"""
DEFAULT = 0x00
SAMPLED = 0x01
@classmethod
def get_default(cls) -> "TraceFlags":
return cls(cls.DEFAULT)
@property
def sampled(self) -> bool:
return bool(self & TraceFlags.SAMPLED)
DEFAULT_TRACE_OPTIONS = TraceFlags.get_default()
class TraceState(typing.Mapping[str, str]):
"""A list of key-value pairs representing vendor-specific trace info.
Keys and values are strings of up to 256 printable US-ASCII characters.
Implementations should conform to the `W3C Trace Context - Tracestate`_
spec, which describes additional restrictions on valid field values.
.. _W3C Trace Context - Tracestate:
https://www.w3.org/TR/trace-context/#tracestate-field
"""
def __init__(
self,
entries: typing.Optional[typing.Sequence[typing.Tuple[str, str]]] = None,
) -> None:
self._dict = OrderedDict() # type: OrderedDict[str, str]
if entries is None:
return
if len(entries) > _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS:
_logger.warning(
"There can't be more than %s key/value pairs.",
_TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS,
)
return
for key, value in entries:
if _is_valid_pair(key, value):
if key in self._dict:
_logger.warning("Duplicate key: %s found.", key)
continue
self._dict[key] = value
else:
_logger.warning("Invalid key/value pair (%s, %s) found.", key, value)
def __contains__(self, item: object) -> bool:
return item in self._dict
def __getitem__(self, key: str) -> str:
return self._dict[key]
def __iter__(self) -> typing.Iterator[str]:
return iter(self._dict)
def __len__(self) -> int:
return len(self._dict)
def __repr__(self) -> str:
pairs = [f"{{key={key}, value={value}}}" for key, value in self._dict.items()]
return str(pairs)
def add(self, key: str, value: str) -> "TraceState":
"""Adds a key-value pair to tracestate. The provided pair should
adhere to w3c tracestate identifiers format.
Args:
key: A valid tracestate key to add
value: A valid tracestate value to add
Returns:
A new TraceState with the modifications applied.
If the provided key-value pair is invalid or results in tracestate
that violates tracecontext specification, they are discarded and
same tracestate will be returned.
"""
if not _is_valid_pair(key, value):
_logger.warning("Invalid key/value pair (%s, %s) found.", key, value)
return self
# There can be a maximum of 32 pairs
if len(self) >= _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS:
_logger.warning("There can't be more 32 key/value pairs.")
return self
# Duplicate entries are not allowed
if key in self._dict:
_logger.warning("The provided key %s already exists.", key)
return self
new_state = [(key, value)] + list(self._dict.items())
return TraceState(new_state)
def update(self, key: str, value: str) -> "TraceState":
"""Updates a key-value pair in tracestate. The provided pair should
adhere to w3c tracestate identifiers format.
Args:
key: A valid tracestate key to update
value: A valid tracestate value to update for key
Returns:
A new TraceState with the modifications applied.
If the provided key-value pair is invalid or results in tracestate
that violates tracecontext specification, they are discarded and
same tracestate will be returned.
"""
if not _is_valid_pair(key, value):
_logger.warning("Invalid key/value pair (%s, %s) found.", key, value)
return self
prev_state = self._dict.copy()
prev_state[key] = value
prev_state.move_to_end(key, last=False)
new_state = list(prev_state.items())
return TraceState(new_state)
def delete(self, key: str) -> "TraceState":
"""Deletes a key-value from tracestate.
Args:
key: A valid tracestate key to remove key-value pair from tracestate
Returns:
A new TraceState with the modifications applied.
If the provided key-value pair is invalid or results in tracestate
that violates tracecontext specification, they are discarded and
same tracestate will be returned.
"""
if key not in self._dict:
_logger.warning("The provided key %s doesn't exist.", key)
return self
prev_state = self._dict.copy()
prev_state.pop(key)
new_state = list(prev_state.items())
return TraceState(new_state)
def to_header(self) -> str:
"""Creates a w3c tracestate header from a TraceState.
Returns:
A string that adheres to the w3c tracestate
header format.
"""
return ",".join(key + "=" + value for key, value in self._dict.items())
@classmethod
def from_header(cls, header_list: typing.List[str]) -> "TraceState":
"""Parses one or more w3c tracestate header into a TraceState.
Args:
header_list: one or more w3c tracestate headers.
Returns:
A valid TraceState that contains values extracted from
the tracestate header.
If the format of one headers is illegal, all values will
be discarded and an empty tracestate will be returned.
If the number of keys is beyond the maximum, all values
will be discarded and an empty tracestate will be returned.
"""
pairs = OrderedDict() # type: OrderedDict[str, str]
for header in header_list:
members: typing.List[str] = re.split(_delimiter_pattern, header)
for member in members:
# empty members are valid, but no need to process further.
if not member:
continue
match = _member_pattern.fullmatch(member)
if not match:
_logger.warning(
"Member doesn't match the w3c identifiers format %s",
member,
)
return cls()
groups: typing.Tuple[str, ...] = match.groups()
key, _eq, value = groups
# duplicate keys are not legal in header
if key in pairs:
return cls()
pairs[key] = value
return cls(list(pairs.items()))
@classmethod
def get_default(cls) -> "TraceState":
return cls()
def keys(self) -> typing.KeysView[str]:
return self._dict.keys()
def items(self) -> typing.ItemsView[str, str]:
return self._dict.items()
def values(self) -> typing.ValuesView[str]:
return self._dict.values()
DEFAULT_TRACE_STATE = TraceState.get_default()
_TRACE_ID_MAX_VALUE = 2**128 - 1
_SPAN_ID_MAX_VALUE = 2**64 - 1
class SpanContext(typing.Tuple[int, int, bool, "TraceFlags", "TraceState", bool]):
"""The state of a Span to propagate between processes.
This class includes the immutable attributes of a :class:`.Span` that must
be propagated to a span's children and across process boundaries.
Args:
trace_id: The ID of the trace that this span belongs to.
span_id: This span's ID.
is_remote: True if propagated from a remote parent.
trace_flags: Trace options to propagate.
trace_state: Tracing-system-specific info to propagate.
"""
def __new__(
cls,
trace_id: int,
span_id: int,
is_remote: bool,
trace_flags: typing.Optional["TraceFlags"] = DEFAULT_TRACE_OPTIONS,
trace_state: typing.Optional["TraceState"] = DEFAULT_TRACE_STATE,
) -> "SpanContext":
if trace_flags is None:
trace_flags = DEFAULT_TRACE_OPTIONS
if trace_state is None:
trace_state = DEFAULT_TRACE_STATE
is_valid = (
INVALID_TRACE_ID < trace_id <= _TRACE_ID_MAX_VALUE
and INVALID_SPAN_ID < span_id <= _SPAN_ID_MAX_VALUE
)
return tuple.__new__(
cls,
(trace_id, span_id, is_remote, trace_flags, trace_state, is_valid),
)
def __getnewargs__(
self,
) -> typing.Tuple[int, int, bool, "TraceFlags", "TraceState"]:
return (
self.trace_id,
self.span_id,
self.is_remote,
self.trace_flags,
self.trace_state,
)
@property
def trace_id(self) -> int:
return self[0] # pylint: disable=unsubscriptable-object
@property
def span_id(self) -> int:
return self[1] # pylint: disable=unsubscriptable-object
@property
def is_remote(self) -> bool:
return self[2] # pylint: disable=unsubscriptable-object
@property
def trace_flags(self) -> "TraceFlags":
return self[3] # pylint: disable=unsubscriptable-object
@property
def trace_state(self) -> "TraceState":
return self[4] # pylint: disable=unsubscriptable-object
@property
def is_valid(self) -> bool:
return self[5] # pylint: disable=unsubscriptable-object
def __setattr__(self, *args: str) -> None:
_logger.debug("Immutable type, ignoring call to set attribute", stack_info=True)
def __delattr__(self, *args: str) -> None:
_logger.debug("Immutable type, ignoring call to set attribute", stack_info=True)
def __repr__(self) -> str:
return f"{type(self).__name__}(trace_id=0x{format_trace_id(self.trace_id)}, span_id=0x{format_span_id(self.span_id)}, trace_flags=0x{self.trace_flags:02x}, trace_state={self.trace_state!r}, is_remote={self.is_remote})"
class NonRecordingSpan(Span):
"""The Span that is used when no Span implementation is available.
All operations are no-op except context propagation.
"""
def __init__(self, context: "SpanContext") -> None:
self._context = context
def get_span_context(self) -> "SpanContext":
return self._context
def is_recording(self) -> bool:
return False
def end(self, end_time: typing.Optional[int] = None) -> None:
pass
def set_attributes(
self, attributes: typing.Dict[str, types.AttributeValue]
) -> None:
pass
def set_attribute(self, key: str, value: types.AttributeValue) -> None:
pass
def add_event(
self,
name: str,
attributes: types.Attributes = None,
timestamp: typing.Optional[int] = None,
) -> None:
pass
def update_name(self, name: str) -> None:
pass
def set_status(
self,
status: typing.Union[Status, StatusCode],
description: typing.Optional[str] = None,
) -> None:
pass
def record_exception(
self,
exception: Exception,
attributes: types.Attributes = None,
timestamp: typing.Optional[int] = None,
escaped: bool = False,
) -> None:
pass
def __repr__(self) -> str:
return f"NonRecordingSpan({self._context!r})"
INVALID_SPAN_ID = 0x0000000000000000
INVALID_TRACE_ID = 0x00000000000000000000000000000000
INVALID_SPAN_CONTEXT = SpanContext(
trace_id=INVALID_TRACE_ID,
span_id=INVALID_SPAN_ID,
is_remote=False,
trace_flags=DEFAULT_TRACE_OPTIONS,
trace_state=DEFAULT_TRACE_STATE,
)
INVALID_SPAN = NonRecordingSpan(INVALID_SPAN_CONTEXT)
def format_trace_id(trace_id: int) -> str:
"""Convenience trace ID formatting method
Args:
trace_id: Trace ID int
Returns:
The trace ID as 32-byte hexadecimal string
"""
return format(trace_id, "032x")
def format_span_id(span_id: int) -> str:
"""Convenience span ID formatting method
Args:
span_id: Span ID int
Returns:
The span ID as 16-byte hexadecimal string
"""
return format(span_id, "016x")
+82
View File
@@ -0,0 +1,82 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import enum
import logging
import typing
logger = logging.getLogger(__name__)
class StatusCode(enum.Enum):
"""Represents the canonical set of status codes of a finished Span."""
UNSET = 0
"""The default status."""
OK = 1
"""The operation has been validated by an Application developer or Operator to have completed successfully."""
ERROR = 2
"""The operation contains an error."""
class Status:
"""Represents the status of a finished Span.
Args:
status_code: The canonical status code that describes the result
status of the operation.
description: An optional description of the status.
"""
def __init__(
self,
status_code: StatusCode = StatusCode.UNSET,
description: typing.Optional[str] = None,
):
self._status_code = status_code
self._description = None
if description:
if not isinstance(description, str):
logger.warning("Invalid status description type, expected str")
return
if status_code is not StatusCode.ERROR:
logger.warning(
"description should only be set when status_code is set to StatusCode.ERROR"
)
return
self._description = description
@property
def status_code(self) -> StatusCode:
"""Represents the canonical status code of a finished Span."""
return self._status_code
@property
def description(self) -> typing.Optional[str]:
"""Status description"""
return self._description
@property
def is_ok(self) -> bool:
"""Returns false if this represents an error, true otherwise."""
return self.is_unset or self._status_code is StatusCode.OK
@property
def is_unset(self) -> bool:
"""Returns true if unset, false otherwise."""
return self._status_code is StatusCode.UNSET
@@ -0,0 +1,35 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import sys
# FIXME: Use importlib.metadata when support for 3.11 is dropped if the rest of
# the supported versions at that time have the same API.
from mysql.opentelemetry.importlib_metadata import ( # type: ignore
EntryPoint,
EntryPoints,
entry_points,
version,
)
path_to_otel, _ = os.path.split(os.path.dirname(__file__))
sys.path.append(os.path.join(path_to_otel, "_dist_info"))
# The importlib-metadata library has introduced breaking changes before to its
# API, this module is kept just to act as a layer between the
# importlib-metadata library and our project if in any case it is necessary to
# do so.
__all__ = ["entry_points", "version", "EntryPoint", "EntryPoints"]
+47
View File
@@ -0,0 +1,47 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from threading import Lock
from typing import Callable
class Once:
"""Execute a function exactly once and block all callers until the function returns
Same as golang's `sync.Once <https://pkg.go.dev/sync#Once>`_
"""
def __init__(self) -> None:
self._lock = Lock()
self._done = False
def do_once(self, func: Callable[[], None]) -> bool:
"""Execute ``func`` if it hasn't been executed or return.
Will block until ``func`` has been called by one thread.
Returns:
Whether or not ``func`` was executed in this call
"""
# fast path, try to avoid locking
if self._done:
return False
with self._lock:
if not self._done:
func()
self._done = True
return True
return False
+50
View File
@@ -0,0 +1,50 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from logging import getLogger
from os import environ
from typing import TYPE_CHECKING, TypeVar, cast
from mysql.opentelemetry.util._importlib_metadata import entry_points
if TYPE_CHECKING:
from mysql.opentelemetry.metrics import MeterProvider
from mysql.opentelemetry.trace import TracerProvider
Provider = TypeVar("Provider", "TracerProvider", "MeterProvider")
logger = getLogger(__name__)
def _load_provider(provider_environment_variable: str, provider: str) -> Provider:
try:
provider_name = cast(
str,
environ.get(provider_environment_variable, f"default_{provider}"),
)
return cast(
Provider,
next( # type: ignore
iter( # type: ignore
entry_points( # type: ignore
group=f"opentelemetry_{provider}",
name=provider_name,
)
)
).load()(),
)
except Exception: # pylint: disable=broad-except
logger.exception("Failed to load configured provider %s", provider)
raise
+76
View File
@@ -0,0 +1,76 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from logging import getLogger
from re import compile, split
from typing import Dict, List, Mapping
from urllib.parse import unquote
from deprecated import deprecated
_logger = getLogger(__name__)
# The following regexes reference this spec: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#specifying-headers-via-environment-variables
# Optional whitespace
_OWS = r"[ \t]*"
# A key contains printable US-ASCII characters except: SP and "(),/:;<=>?@[\]{}
_KEY_FORMAT = r"[\x21\x23-\x27\x2a\x2b\x2d\x2e\x30-\x39\x41-\x5a\x5e-\x7a\x7c\x7e]+"
# A value contains a URL-encoded UTF-8 string. The encoded form can contain any
# printable US-ASCII characters (0x20-0x7f) other than SP, DEL, and ",;/
_VALUE_FORMAT = r"[\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]*"
# A key-value is key=value, with optional whitespace surrounding key and value
_KEY_VALUE_FORMAT = rf"{_OWS}{_KEY_FORMAT}{_OWS}={_OWS}{_VALUE_FORMAT}{_OWS}"
_HEADER_PATTERN = compile(_KEY_VALUE_FORMAT)
_DELIMITER_PATTERN = compile(r"[ \t]*,[ \t]*")
_BAGGAGE_PROPERTY_FORMAT = rf"{_KEY_VALUE_FORMAT}|{_OWS}{_KEY_FORMAT}{_OWS}"
# pylint: disable=invalid-name
@deprecated(version="1.15.0", reason="You should use parse_env_headers") # type: ignore
def parse_headers(s: str) -> Mapping[str, str]:
return parse_env_headers(s)
def parse_env_headers(s: str) -> Mapping[str, str]:
"""
Parse ``s``, which is a ``str`` instance containing HTTP headers encoded
for use in ENV variables per the W3C Baggage HTTP header format at
https://www.w3.org/TR/baggage/#baggage-http-header-format, except that
additional semi-colon delimited metadata is not supported.
"""
headers: Dict[str, str] = {}
headers_list: List[str] = split(_DELIMITER_PATTERN, s)
for header in headers_list:
if not header: # empty string
continue
match = _HEADER_PATTERN.fullmatch(header.strip())
if not match:
_logger.warning(
"Header format invalid! Header values in environment variables must be "
"URL encoded per the OpenTelemetry Protocol Exporter specification: %s",
header,
)
continue
# value may contain any number of `=`
name, value = match.string.split("=", 1)
name = unquote(name).strip().lower()
value = unquote(value).strip()
headers[name] = value
return headers

Some files were not shown because too many files have changed in this diff Show More