././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1679663596.5651188 py_zipkin-1.2.8/0000755000175100001730000000000000000000000014340 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663591.0 py_zipkin-1.2.8/CHANGELOG.rst0000644000175100001730000001550300000000000016365 0ustar00runnerdocker000000000000001.2.8 (2023-03-23) ------------------- - Add back exports in py_zipkin.encoding - Fix mypy tests 1.2.7 (2023-02-06) ------------------- - Drop support for Python 3.6 1.2.6 (2023-02-06) ------------------- - Drop support for V1_THRIFT encoding 1.0.0 (2022-06-09) ------------------- - Droop Python 2.7 support (minimal supported python version is 3.5) - Recompile protobuf using version 3.19 0.21.0 (2021-03-17) ------------------- - The default encoding is now V2 JSON. If you want to keep the old V1 thrift encoding you'll need to specify it. 0.20.2 (2021-03-11) ------------------- - Don't crash when annotating exceptions that cannot be str()'d 0.20.1 (2020-10-27) ------------------- - Support PRODUCER and CONSUMER spans 0.20.0 (2020-03-09) ------------------- - Add create_http_headers helper 0.19.0 (2020-02-28) ------------------- - Add zipkin_span.add_annotation() method - Add autoinstrumentation for python Threads - Allow creating a copy of Tracer - Add extract_zipkin_attrs_from_headers() helper 0.18.7 (2020-01-15) ------------------- - Expose encoding.create_endpoint helper 0.18.6 (2019-09-23) ------------------- - Ensure tags are strings when using V2_JSON encoding 0.18.5 (2019-08-08) ------------------- - Add testing.MockTransportHandler module 0.18.4 (2019-08-02) ------------------- - Fix thriftpy2 import to allow cython module 0.18.3 (2019-05-15) ------------------- - Fix unicode bug when decoding thrift tag strings 0.18.2 (2019-03-26) ------------------- - Handled exception while emitting trace and log the error - Ensure tracer is cleared regardless span of emit outcome 0.18.1 (2019-02-22) ------------------- - Fix ThreadLocalStack() bug introduced in 0.18.0 0.18.0 (2019-02-13) ------------------- - Fix multithreading issues - Added Tracer module 0.17.1 (2019-02-05) ------------------- - Ignore transport_handler overrides in an inner span since that causes spans to be dropped. 0.17.0 (2019-01-25) ------------------- - Support python 3.7 - py-zipkin now depends on thriftpy2 rather than thriftpy. They can coexist in the same codebase, so it should be safe to upgrade. 0.16.1 (2018-11-16) ------------------- - Handle null timestamps when decoding thrift traces 0.16.0 (2018-11-13) ------------------- - py_zipkin is now able to convert V1 thrift spans to V2 JSON 0.15.1 (2018-10-31) ------------------- - Changed DeprecationWarnings to logging.warning 0.15.0 (2018-10-22) ------------------- - Added support for V2 JSON encoding. - Fixed TransportHandler bug that was affecting also V1 JSON. 0.14.1 (2018-10-09) ------------------- - Fixed memory leak introduced in 0.13.0. 0.14.0 (2018-10-01) ------------------- - Support JSON encoding for V1 spans. - Allow overriding the span_name after creation. 0.13.0 (2018-06-25) ------------------- - Removed deprecated `zipkin_logger.debug()` interface. - `py_zipkin.stack` was renamed as `py_zipkin.storage`. If you were importing this module, you'll need to update your code. 0.12.0 (2018-05-29) ------------------- - Support max payload size for transport handlers. - Transport handlers should now be implemented as classes extending py_zipkin.transport.BaseTransportHandler. 0.11.2 (2018-05-23) ------------------- - Don't overwrite passed in annotations 0.11.1 (2018-05-23) ------------------- - Add binary annotations to the span even if the request is not being sampled. This fixes binary annotations for firehose spans. 0.11.0 (2018-02-08) ------------------- - Add support for "firehose mode", which logs 100% of the spans regardless of sample rate. 0.10.1 (2018-02-05) ------------------- - context_stack will now default to `ThreadLocalStack()` if passed as `None` 0.10.0 (2018-02-05) ------------------- - Add support for using explicit in-process context storage instead of using thread_local. This allows you to use py_zipkin in cooperative multitasking environments e.g. asyncio - `py_zipkin.thread_local` is now deprecated. Instead use `py_zipkin.stack.ThreadLocalStack()` - TraceId and SpanId generation performance improvements. - 128-bit TraceIds now start with an epoch timestamp to support easy interop with AWS X-Ray 0.9.0 (2017-07-31) ------------------ - Add batch span sending. Note that spans are now sent in lists. 0.8.3 (2017-07-10) ------------------ - Be defensive about having logging handlers configured to avoid throwing NullHandler attribute errors 0.8.2 (2017-06-30) ------------------ - Don't log ss and sr annotations when in a client span context - Add error binary annotation if an exception occurs 0.8.1 (2017-06-16) ------------------ - Fixed server send timing to more accurately reflect when server send actually occurs. - Replaced logging_start annotation with logging_end 0.8.0 (2017-06-01) ------------------ - Added 128-bit trace id support - Added ability to explicitly specify host for a span - Added exception handling if host can't be determined automatically - SERVER_ADDR ('sa') binary annotations can be added to spans - py36 support 0.7.1 (2017-05-01) ------------------ - Fixed a bug where `update_binary_annotations` would fail for a child span in a trace that is not being sampled 0.7.0 (2017-03-06) ------------------ - Simplify `update_binary_annotations` for both root and non-root spans 0.6.0 (2017-02-03) ------------------ - Added support for forcing `zipkin_span` to report timestamp/duration. Changes API of `zipkin_span`, but defaults back to existing behavior. 0.5.0 (2017-02-01) ------------------ - Properly set timestamp/duration on server and local spans - Updated thrift spec to include these new fields - The `zipkin_span` entrypoint should be backwards compatible 0.4.4 (2016-11-29) ------------------ - Add optional annotation for when Zipkin logging starts 0.4.3 (2016-11-04) ------------------ - Fix bug in zipkin_span decorator 0.4.2 (2016-11-01) ------------------ - Be defensive about transport_handler when logging spans. 0.4.1 (2016-10-24) ------------------ - Add ability to override span_id when creating new ZipkinAttrs. 0.4.0 (2016-10-20) ------------------ - Added `start` and `stop` functions as friendlier versions of the __enter__ and __exit__ functions. 0.3.1 (2016-09-30) ------------------ - Adds new param to thrift.create_endpoint allowing creation of thrift Endpoint objects on a proxy machine representing another host. 0.2.1 (2016-09-30) ------------------ - Officially "release" v0.2.0. Accidentally pushed a v0.2.0 without the proper version bump, so v0.2.1 is the new real version. Please use this instead of v0.2.0. 0.2.0 (2016-09-30) ------------------ - Fix problem where if zipkin_attrs and sample_rate were passed, but zipkin_attrs.is_sampled=True, new zipkin_attrs were being generated. 0.1.2 (2016-09-29) ------------------ - Fix sampling algorithm that always sampled for rates > 50% 0.1.1 (2016-07-05) ------------------ - First py_zipkin version with context manager/decorator functionality. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663591.0 py_zipkin-1.2.8/LICENSE.txt0000644000175100001730000002511700000000000016171 0ustar00runnerdocker00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2015 Yelp Inc 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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663591.0 py_zipkin-1.2.8/MANIFEST.in0000644000175100001730000000005000000000000016071 0ustar00runnerdocker00000000000000include README.md include CHANGELOG.rst ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1679663596.5611188 py_zipkin-1.2.8/PKG-INFO0000644000175100001730000004653100000000000015446 0ustar00runnerdocker00000000000000Metadata-Version: 2.1 Name: py_zipkin Version: 1.2.8 Summary: Library for using Zipkin in Python. Home-page: https://github.com/Yelp/py_zipkin Author: Yelp, Inc. Author-email: opensource+py-zipkin@yelp.com License: Copyright Yelp 2019 Description: [![Build Status](https://travis-ci.org/Yelp/py_zipkin.svg?branch=master)](https://travis-ci.org/Yelp/py_zipkin) [![Coverage Status](https://img.shields.io/coveralls/Yelp/py_zipkin.svg)](https://coveralls.io/r/Yelp/py_zipkin) [![PyPi version](https://img.shields.io/pypi/v/py_zipkin.svg)](https://pypi.python.org/pypi/py_zipkin/) [![Supported Python versions](https://img.shields.io/pypi/pyversions/py_zipkin.svg)](https://pypi.python.org/pypi/py_zipkin/) py_zipkin --------- py_zipkin provides a context manager/decorator along with some utilities to facilitate the usage of Zipkin in Python applications. Install ------- ``` pip install py_zipkin ``` Usage ----- py_zipkin requires a `transport_handler` object that handles logging zipkin messages to a central logging service such as kafka or scribe. `py_zipkin.zipkin.zipkin_span` is the main tool for starting zipkin traces or logging spans inside an ongoing trace. zipkin_span can be used as a context manager or a decorator. #### Usage #1: Start a trace with a given sampling rate ```python from py_zipkin.zipkin import zipkin_span def some_function(a, b): with zipkin_span( service_name='my_service', span_name='my_span_name', transport_handler=some_handler, port=42, sample_rate=0.05, # Value between 0.0 and 100.0 ): do_stuff(a, b) ``` #### Usage #2: Trace a service call The difference between this and Usage #1 is that the zipkin_attrs are calculated separately and passed in, thus negating the need of the sample_rate param. ```python # Define a pyramid tween def tween(request): zipkin_attrs = some_zipkin_attr_creator(request) with zipkin_span( service_name='my_service', span_name='my_span_name', zipkin_attrs=zipkin_attrs, transport_handler=some_handler, port=22, ) as zipkin_context: response = handler(request) zipkin_context.update_binary_annotations( some_binary_annotations) return response ``` #### Usage #3: Log a span inside an ongoing trace This can be also be used inside itself to produce continuously nested spans. ```python @zipkin_span(service_name='my_service', span_name='some_function') def some_function(a, b): return do_stuff(a, b) ``` #### Other utilities `zipkin_span.update_binary_annotations()` can be used inside a zipkin trace to add to the existing set of binary annotations. ```python def some_function(a, b): with zipkin_span( service_name='my_service', span_name='some_function', transport_handler=some_handler, port=42, sample_rate=0.05, ) as zipkin_context: result = do_stuff(a, b) zipkin_context.update_binary_annotations({'result': result}) ``` `zipkin_span.add_sa_binary_annotation()` can be used to add a binary annotation to the current span with the key 'sa'. This function allows the user to specify the destination address of the service being called (useful if the destination doesn't support zipkin). See http://zipkin.io/pages/data_model.html for more information on the 'sa' binary annotation. > NOTE: the V2 span format only support 1 "sa" endpoint (represented by remoteEndpoint) > so `add_sa_binary_annotation` now raises `ValueError` if you try to set multiple "sa" > annotations for the same span. ```python def some_function(): with zipkin_span( service_name='my_service', span_name='some_function', transport_handler=some_handler, port=42, sample_rate=0.05, ) as zipkin_context: make_call_to_non_instrumented_service() zipkin_context.add_sa_binary_annotation( port=123, service_name='non_instrumented_service', host='12.34.56.78', ) ``` `create_http_headers_for_new_span()` creates a set of HTTP headers that can be forwarded in a request to another service. ```python headers = {} headers.update(create_http_headers_for_new_span()) http_client.get( path='some_url', headers=headers, ) ``` Transport --------- py_zipkin (for the moment) thrift-encodes spans. The actual transport layer is pluggable, though. The recommended way to implement a new transport handler is to subclass `py_zipkin.transport.BaseTransportHandler` and implement the `send` and `get_max_payload_bytes` methods. `send` receives an already encoded thrift list as argument. `get_max_payload_bytes` should return the maximum payload size supported by your transport, or `None` if you can send arbitrarily big messages. The simplest way to get spans to the collector is via HTTP POST. Here's an example of a simple HTTP transport using the `requests` library. This assumes your Zipkin collector is running at localhost:9411. > NOTE: older versions of py_zipkin suggested implementing the transport handler > as a function with a single argument. That's still supported and should work > with the current py_zipkin version, but it's deprecated. ```python import requests from py_zipkin.transport import BaseTransportHandler class HttpTransport(BaseTransportHandler): def get_max_payload_bytes(self): return None def send(self, encoded_span): # The collector expects a thrift-encoded list of spans. requests.post( 'http://localhost:9411/api/v1/spans', data=encoded_span, headers={'Content-Type': 'application/x-thrift'}, ) ``` If you have the ability to send spans over Kafka (more like what you might do in production), you'd do something like the following, using the [kafka-python](https://pypi.python.org/pypi/kafka-python) package: ```python from kafka import SimpleProducer, KafkaClient from py_zipkin.transport import BaseTransportHandler class KafkaTransport(BaseTransportHandler): def get_max_payload_bytes(self): # By default Kafka rejects messages bigger than 1000012 bytes. return 1000012 def send(self, message): kafka_client = KafkaClient('{}:{}'.format('localhost', 9092)) producer = SimpleProducer(kafka_client) producer.send_messages('kafka_topic_name', message) ``` Using in multithreading environments ------------------------------------ If you want to use py_zipkin in a cooperative multithreading environment, e.g. asyncio, you need to explicitly pass an instance of `py_zipkin.storage.Stack` as parameter `context_stack` for `zipkin_span` and `create_http_headers_for_new_span`. By default, py_zipkin uses a thread local storage for the attributes, which is defined in `py_zipkin.storage.ThreadLocalStack`. Additionally, you'll also need to explicitly pass an instance of `py_zipkin.storage.SpanStorage` as parameter `span_storage` to `zipkin_span`. ```python from py_zipkin.zipkin import zipkin_span from py_zipkin.storage import Stack from py_zipkin.storage import SpanStorage def my_function(): context_stack = Stack() span_storage = SpanStorage() await my_function(context_stack, span_storage) async def my_function(context_stack, span_storage): with zipkin_span( service_name='my_service', span_name='some_function', transport_handler=some_handler, port=42, sample_rate=0.05, context_stack=context_stack, span_storage=span_storage, ): result = do_stuff(a, b) ``` Firehose mode [EXPERIMENTAL] ---------------------------- "Firehose mode" records 100% of the spans, regardless of sampling rate. This is useful if you want to treat these spans differently, e.g. send them to a different backend that has limited retention. It works in tandem with normal operation, however there may be additional overhead. In order to use this, you add a `firehose_handler` just like you add a `transport_handler`. This feature should be considered experimental and may be removed at any time without warning. If you do use this, be sure to send asynchronously to avoid excess overhead for every request. License ------- Copyright (c) 2018, Yelp, Inc. All Rights reserved. Apache v2 1.2.8 (2023-03-23) ------------------- - Add back exports in py_zipkin.encoding - Fix mypy tests 1.2.7 (2023-02-06) ------------------- - Drop support for Python 3.6 1.2.6 (2023-02-06) ------------------- - Drop support for V1_THRIFT encoding 1.0.0 (2022-06-09) ------------------- - Droop Python 2.7 support (minimal supported python version is 3.5) - Recompile protobuf using version 3.19 0.21.0 (2021-03-17) ------------------- - The default encoding is now V2 JSON. If you want to keep the old V1 thrift encoding you'll need to specify it. 0.20.2 (2021-03-11) ------------------- - Don't crash when annotating exceptions that cannot be str()'d 0.20.1 (2020-10-27) ------------------- - Support PRODUCER and CONSUMER spans 0.20.0 (2020-03-09) ------------------- - Add create_http_headers helper 0.19.0 (2020-02-28) ------------------- - Add zipkin_span.add_annotation() method - Add autoinstrumentation for python Threads - Allow creating a copy of Tracer - Add extract_zipkin_attrs_from_headers() helper 0.18.7 (2020-01-15) ------------------- - Expose encoding.create_endpoint helper 0.18.6 (2019-09-23) ------------------- - Ensure tags are strings when using V2_JSON encoding 0.18.5 (2019-08-08) ------------------- - Add testing.MockTransportHandler module 0.18.4 (2019-08-02) ------------------- - Fix thriftpy2 import to allow cython module 0.18.3 (2019-05-15) ------------------- - Fix unicode bug when decoding thrift tag strings 0.18.2 (2019-03-26) ------------------- - Handled exception while emitting trace and log the error - Ensure tracer is cleared regardless span of emit outcome 0.18.1 (2019-02-22) ------------------- - Fix ThreadLocalStack() bug introduced in 0.18.0 0.18.0 (2019-02-13) ------------------- - Fix multithreading issues - Added Tracer module 0.17.1 (2019-02-05) ------------------- - Ignore transport_handler overrides in an inner span since that causes spans to be dropped. 0.17.0 (2019-01-25) ------------------- - Support python 3.7 - py-zipkin now depends on thriftpy2 rather than thriftpy. They can coexist in the same codebase, so it should be safe to upgrade. 0.16.1 (2018-11-16) ------------------- - Handle null timestamps when decoding thrift traces 0.16.0 (2018-11-13) ------------------- - py_zipkin is now able to convert V1 thrift spans to V2 JSON 0.15.1 (2018-10-31) ------------------- - Changed DeprecationWarnings to logging.warning 0.15.0 (2018-10-22) ------------------- - Added support for V2 JSON encoding. - Fixed TransportHandler bug that was affecting also V1 JSON. 0.14.1 (2018-10-09) ------------------- - Fixed memory leak introduced in 0.13.0. 0.14.0 (2018-10-01) ------------------- - Support JSON encoding for V1 spans. - Allow overriding the span_name after creation. 0.13.0 (2018-06-25) ------------------- - Removed deprecated `zipkin_logger.debug()` interface. - `py_zipkin.stack` was renamed as `py_zipkin.storage`. If you were importing this module, you'll need to update your code. 0.12.0 (2018-05-29) ------------------- - Support max payload size for transport handlers. - Transport handlers should now be implemented as classes extending py_zipkin.transport.BaseTransportHandler. 0.11.2 (2018-05-23) ------------------- - Don't overwrite passed in annotations 0.11.1 (2018-05-23) ------------------- - Add binary annotations to the span even if the request is not being sampled. This fixes binary annotations for firehose spans. 0.11.0 (2018-02-08) ------------------- - Add support for "firehose mode", which logs 100% of the spans regardless of sample rate. 0.10.1 (2018-02-05) ------------------- - context_stack will now default to `ThreadLocalStack()` if passed as `None` 0.10.0 (2018-02-05) ------------------- - Add support for using explicit in-process context storage instead of using thread_local. This allows you to use py_zipkin in cooperative multitasking environments e.g. asyncio - `py_zipkin.thread_local` is now deprecated. Instead use `py_zipkin.stack.ThreadLocalStack()` - TraceId and SpanId generation performance improvements. - 128-bit TraceIds now start with an epoch timestamp to support easy interop with AWS X-Ray 0.9.0 (2017-07-31) ------------------ - Add batch span sending. Note that spans are now sent in lists. 0.8.3 (2017-07-10) ------------------ - Be defensive about having logging handlers configured to avoid throwing NullHandler attribute errors 0.8.2 (2017-06-30) ------------------ - Don't log ss and sr annotations when in a client span context - Add error binary annotation if an exception occurs 0.8.1 (2017-06-16) ------------------ - Fixed server send timing to more accurately reflect when server send actually occurs. - Replaced logging_start annotation with logging_end 0.8.0 (2017-06-01) ------------------ - Added 128-bit trace id support - Added ability to explicitly specify host for a span - Added exception handling if host can't be determined automatically - SERVER_ADDR ('sa') binary annotations can be added to spans - py36 support 0.7.1 (2017-05-01) ------------------ - Fixed a bug where `update_binary_annotations` would fail for a child span in a trace that is not being sampled 0.7.0 (2017-03-06) ------------------ - Simplify `update_binary_annotations` for both root and non-root spans 0.6.0 (2017-02-03) ------------------ - Added support for forcing `zipkin_span` to report timestamp/duration. Changes API of `zipkin_span`, but defaults back to existing behavior. 0.5.0 (2017-02-01) ------------------ - Properly set timestamp/duration on server and local spans - Updated thrift spec to include these new fields - The `zipkin_span` entrypoint should be backwards compatible 0.4.4 (2016-11-29) ------------------ - Add optional annotation for when Zipkin logging starts 0.4.3 (2016-11-04) ------------------ - Fix bug in zipkin_span decorator 0.4.2 (2016-11-01) ------------------ - Be defensive about transport_handler when logging spans. 0.4.1 (2016-10-24) ------------------ - Add ability to override span_id when creating new ZipkinAttrs. 0.4.0 (2016-10-20) ------------------ - Added `start` and `stop` functions as friendlier versions of the __enter__ and __exit__ functions. 0.3.1 (2016-09-30) ------------------ - Adds new param to thrift.create_endpoint allowing creation of thrift Endpoint objects on a proxy machine representing another host. 0.2.1 (2016-09-30) ------------------ - Officially "release" v0.2.0. Accidentally pushed a v0.2.0 without the proper version bump, so v0.2.1 is the new real version. Please use this instead of v0.2.0. 0.2.0 (2016-09-30) ------------------ - Fix problem where if zipkin_attrs and sample_rate were passed, but zipkin_attrs.is_sampled=True, new zipkin_attrs were being generated. 0.1.2 (2016-09-29) ------------------ - Fix sampling algorithm that always sampled for rates > 50% 0.1.1 (2016-07-05) ------------------ - First py_zipkin version with context manager/decorator functionality. Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Provides: py_zipkin Requires-Python: >=3.7 Description-Content-Type: text/markdown Provides-Extra: protobuf ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663591.0 py_zipkin-1.2.8/README.md0000644000175100001730000001760400000000000015627 0ustar00runnerdocker00000000000000[![Build Status](https://travis-ci.org/Yelp/py_zipkin.svg?branch=master)](https://travis-ci.org/Yelp/py_zipkin) [![Coverage Status](https://img.shields.io/coveralls/Yelp/py_zipkin.svg)](https://coveralls.io/r/Yelp/py_zipkin) [![PyPi version](https://img.shields.io/pypi/v/py_zipkin.svg)](https://pypi.python.org/pypi/py_zipkin/) [![Supported Python versions](https://img.shields.io/pypi/pyversions/py_zipkin.svg)](https://pypi.python.org/pypi/py_zipkin/) py_zipkin --------- py_zipkin provides a context manager/decorator along with some utilities to facilitate the usage of Zipkin in Python applications. Install ------- ``` pip install py_zipkin ``` Usage ----- py_zipkin requires a `transport_handler` object that handles logging zipkin messages to a central logging service such as kafka or scribe. `py_zipkin.zipkin.zipkin_span` is the main tool for starting zipkin traces or logging spans inside an ongoing trace. zipkin_span can be used as a context manager or a decorator. #### Usage #1: Start a trace with a given sampling rate ```python from py_zipkin.zipkin import zipkin_span def some_function(a, b): with zipkin_span( service_name='my_service', span_name='my_span_name', transport_handler=some_handler, port=42, sample_rate=0.05, # Value between 0.0 and 100.0 ): do_stuff(a, b) ``` #### Usage #2: Trace a service call The difference between this and Usage #1 is that the zipkin_attrs are calculated separately and passed in, thus negating the need of the sample_rate param. ```python # Define a pyramid tween def tween(request): zipkin_attrs = some_zipkin_attr_creator(request) with zipkin_span( service_name='my_service', span_name='my_span_name', zipkin_attrs=zipkin_attrs, transport_handler=some_handler, port=22, ) as zipkin_context: response = handler(request) zipkin_context.update_binary_annotations( some_binary_annotations) return response ``` #### Usage #3: Log a span inside an ongoing trace This can be also be used inside itself to produce continuously nested spans. ```python @zipkin_span(service_name='my_service', span_name='some_function') def some_function(a, b): return do_stuff(a, b) ``` #### Other utilities `zipkin_span.update_binary_annotations()` can be used inside a zipkin trace to add to the existing set of binary annotations. ```python def some_function(a, b): with zipkin_span( service_name='my_service', span_name='some_function', transport_handler=some_handler, port=42, sample_rate=0.05, ) as zipkin_context: result = do_stuff(a, b) zipkin_context.update_binary_annotations({'result': result}) ``` `zipkin_span.add_sa_binary_annotation()` can be used to add a binary annotation to the current span with the key 'sa'. This function allows the user to specify the destination address of the service being called (useful if the destination doesn't support zipkin). See http://zipkin.io/pages/data_model.html for more information on the 'sa' binary annotation. > NOTE: the V2 span format only support 1 "sa" endpoint (represented by remoteEndpoint) > so `add_sa_binary_annotation` now raises `ValueError` if you try to set multiple "sa" > annotations for the same span. ```python def some_function(): with zipkin_span( service_name='my_service', span_name='some_function', transport_handler=some_handler, port=42, sample_rate=0.05, ) as zipkin_context: make_call_to_non_instrumented_service() zipkin_context.add_sa_binary_annotation( port=123, service_name='non_instrumented_service', host='12.34.56.78', ) ``` `create_http_headers_for_new_span()` creates a set of HTTP headers that can be forwarded in a request to another service. ```python headers = {} headers.update(create_http_headers_for_new_span()) http_client.get( path='some_url', headers=headers, ) ``` Transport --------- py_zipkin (for the moment) thrift-encodes spans. The actual transport layer is pluggable, though. The recommended way to implement a new transport handler is to subclass `py_zipkin.transport.BaseTransportHandler` and implement the `send` and `get_max_payload_bytes` methods. `send` receives an already encoded thrift list as argument. `get_max_payload_bytes` should return the maximum payload size supported by your transport, or `None` if you can send arbitrarily big messages. The simplest way to get spans to the collector is via HTTP POST. Here's an example of a simple HTTP transport using the `requests` library. This assumes your Zipkin collector is running at localhost:9411. > NOTE: older versions of py_zipkin suggested implementing the transport handler > as a function with a single argument. That's still supported and should work > with the current py_zipkin version, but it's deprecated. ```python import requests from py_zipkin.transport import BaseTransportHandler class HttpTransport(BaseTransportHandler): def get_max_payload_bytes(self): return None def send(self, encoded_span): # The collector expects a thrift-encoded list of spans. requests.post( 'http://localhost:9411/api/v1/spans', data=encoded_span, headers={'Content-Type': 'application/x-thrift'}, ) ``` If you have the ability to send spans over Kafka (more like what you might do in production), you'd do something like the following, using the [kafka-python](https://pypi.python.org/pypi/kafka-python) package: ```python from kafka import SimpleProducer, KafkaClient from py_zipkin.transport import BaseTransportHandler class KafkaTransport(BaseTransportHandler): def get_max_payload_bytes(self): # By default Kafka rejects messages bigger than 1000012 bytes. return 1000012 def send(self, message): kafka_client = KafkaClient('{}:{}'.format('localhost', 9092)) producer = SimpleProducer(kafka_client) producer.send_messages('kafka_topic_name', message) ``` Using in multithreading environments ------------------------------------ If you want to use py_zipkin in a cooperative multithreading environment, e.g. asyncio, you need to explicitly pass an instance of `py_zipkin.storage.Stack` as parameter `context_stack` for `zipkin_span` and `create_http_headers_for_new_span`. By default, py_zipkin uses a thread local storage for the attributes, which is defined in `py_zipkin.storage.ThreadLocalStack`. Additionally, you'll also need to explicitly pass an instance of `py_zipkin.storage.SpanStorage` as parameter `span_storage` to `zipkin_span`. ```python from py_zipkin.zipkin import zipkin_span from py_zipkin.storage import Stack from py_zipkin.storage import SpanStorage def my_function(): context_stack = Stack() span_storage = SpanStorage() await my_function(context_stack, span_storage) async def my_function(context_stack, span_storage): with zipkin_span( service_name='my_service', span_name='some_function', transport_handler=some_handler, port=42, sample_rate=0.05, context_stack=context_stack, span_storage=span_storage, ): result = do_stuff(a, b) ``` Firehose mode [EXPERIMENTAL] ---------------------------- "Firehose mode" records 100% of the spans, regardless of sampling rate. This is useful if you want to treat these spans differently, e.g. send them to a different backend that has limited retention. It works in tandem with normal operation, however there may be additional overhead. In order to use this, you add a `firehose_handler` just like you add a `transport_handler`. This feature should be considered experimental and may be removed at any time without warning. If you do use this, be sure to send asynchronously to avoid excess overhead for every request. License ------- Copyright (c) 2018, Yelp, Inc. All Rights reserved. Apache v2 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1679663596.5611188 py_zipkin-1.2.8/py_zipkin/0000755000175100001730000000000000000000000016354 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663591.0 py_zipkin-1.2.8/py_zipkin/__init__.py0000644000175100001730000000041200000000000020462 0ustar00runnerdocker00000000000000# Export useful functions and types from private modules. from py_zipkin.encoding._types import Encoding # noqa from py_zipkin.encoding._types import Kind # noqa from py_zipkin.storage import get_default_tracer # noqa from py_zipkin.storage import Tracer # noqa ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1679663596.5611188 py_zipkin-1.2.8/py_zipkin/encoding/0000755000175100001730000000000000000000000020142 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663591.0 py_zipkin-1.2.8/py_zipkin/encoding/__init__.py0000644000175100001730000000752400000000000022263 0ustar00runnerdocker00000000000000import json from typing import Optional from typing import Union from py_zipkin.encoding._decoders import get_decoder # noqa: F401 from py_zipkin.encoding._encoders import get_encoder # noqa: F401 from py_zipkin.encoding._helpers import create_endpoint # noqa: F401 from py_zipkin.encoding._helpers import Endpoint # noqa: F401 from py_zipkin.encoding._helpers import Span # noqa: F401 from py_zipkin.encoding._types import Encoding from py_zipkin.exception import ZipkinError _V2_ATTRIBUTES = ["tags", "localEndpoint", "remoteEndpoint", "shared", "kind"] def detect_span_version_and_encoding(message: Union[bytes, str]) -> Encoding: """Returns the span type and encoding for the message provided. The logic in this function is a Python port of https://github.com/openzipkin/zipkin/blob/master/zipkin/src/main/java/zipkin/internal/DetectingSpanDecoder.java :param message: span to perform operations on. :type message: byte array :returns: span encoding. :rtype: Encoding """ # In case message is sent in as non-bytearray format, # safeguard convert to bytearray before handling if isinstance(message, str): message = message.encode("utf-8") # pragma: no cover if len(message) < 2: raise ZipkinError("Invalid span format. Message too short.") # Check for binary format if message[0] <= 16: if message[0] == 10 and message[1:2][0] != 0: return Encoding.V2_PROTO3 return Encoding.V1_THRIFT str_msg = message.decode("utf-8") # JSON case for list of spans if str_msg[0] == "[": span_list = json.loads(str_msg) if len(span_list) > 0: # Assumption: All spans in a list are the same version # Logic: Search for identifying fields in all spans, if any span can # be strictly identified to a version, return that version. # Otherwise, if no spans could be strictly identified, default to V2. for span in span_list: if any(word in span for word in _V2_ATTRIBUTES): return Encoding.V2_JSON elif "binaryAnnotations" in span or ( "annotations" in span and "endpoint" in span["annotations"] ): return Encoding.V1_JSON return Encoding.V2_JSON raise ZipkinError("Unknown or unsupported span encoding") def convert_spans( spans: bytes, output_encoding: Encoding, input_encoding: Optional[Encoding] = None ) -> Union[str, bytes]: """Converts encoded spans to a different encoding. param spans: encoded input spans. type spans: byte array param output_encoding: desired output encoding. type output_encoding: Encoding param input_encoding: optional input encoding. If this is not specified, it'll try to understand the encoding automatically by inspecting the input spans. type input_encoding: Encoding :returns: encoded spans. :rtype: byte array """ if not isinstance(input_encoding, Encoding): input_encoding = detect_span_version_and_encoding(message=spans) if input_encoding == output_encoding: return spans raise NotImplementedError( f"Conversion from {input_encoding} to " + f"{output_encoding} is not currently supported." ) # TODO: This code is currently unreachable because no decoders are implemented. # Please uncomment after implementing some. # decoder = get_decoder(input_encoding) # encoder = get_encoder(output_encoding) # decoded_spans = decoder.decode_spans(spans) # output_spans: List[Union[str, bytes]] = [] # # Encode each indivicual span # for span in decoded_spans: # output_spans.append(encoder.encode_span(span)) # # Outputs from encoder.encode_span() can be easily concatenated in a list # return encoder.encode_queue(output_spans) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663591.0 py_zipkin-1.2.8/py_zipkin/encoding/_decoders.py0000644000175100001730000000227700000000000022453 0ustar00runnerdocker00000000000000import logging from typing import List from py_zipkin.encoding._helpers import Span from py_zipkin.encoding._types import Encoding from py_zipkin.exception import ZipkinError log = logging.getLogger("py_zipkin.encoding") def get_decoder(encoding: Encoding) -> "IDecoder": """Creates encoder object for the given encoding. :param encoding: desired output encoding protocol :type encoding: Encoding :return: corresponding IDecoder object :rtype: IDecoder """ if encoding == Encoding.V1_THRIFT: raise NotImplementedError(f"{encoding} decoding no longer supported") if encoding == Encoding.V1_JSON: raise NotImplementedError(f"{encoding} decoding not yet implemented") if encoding == Encoding.V2_JSON: raise NotImplementedError(f"{encoding} decoding not yet implemented") raise ZipkinError(f"Unknown encoding: {encoding}") class IDecoder: """Decoder interface.""" def decode_spans(self, spans: bytes) -> List[Span]: """Decodes an encoded list of spans. :param spans: encoded list of spans :type spans: bytes :return: list of spans :rtype: list of Span """ raise NotImplementedError() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663591.0 py_zipkin-1.2.8/py_zipkin/encoding/_encoders.py0000644000175100001730000002525100000000000022462 0ustar00runnerdocker00000000000000import json from typing import Dict from typing import List from typing import Optional from typing import Union from typing_extensions import TypedDict from typing_extensions import TypeGuard from py_zipkin.encoding import protobuf from py_zipkin.encoding._helpers import Endpoint from py_zipkin.encoding._helpers import Span from py_zipkin.encoding._types import Encoding from py_zipkin.encoding._types import Kind from py_zipkin.exception import ZipkinError from py_zipkin.util import unsigned_hex_to_signed_int def get_encoder(encoding: Encoding) -> "IEncoder": """Creates encoder object for the given encoding. :param encoding: desired output encoding protocol. :type encoding: Encoding :return: corresponding IEncoder object :rtype: IEncoder """ if encoding == Encoding.V1_THRIFT: raise NotImplementedError(f"{encoding} encoding no longer supported") if encoding == Encoding.V1_JSON: return _V1JSONEncoder() if encoding == Encoding.V2_JSON: return _V2JSONEncoder() if encoding == Encoding.V2_PROTO3: return _V2ProtobufEncoder() raise ZipkinError(f"Unknown encoding: {encoding}") class IEncoder: """Encoder interface.""" def fits( self, current_count: int, current_size: int, max_size: int, new_span: Union[str, bytes], ) -> bool: """Returns whether the new span will fit in the list. :param current_count: number of spans already in the list. :type current_count: int :param current_size: sum of the sizes of all the spans already in the list. :type current_size: int :param max_size: max supported transport payload size. :type max_size: int :param new_span: encoded span object that we want to add the the list. :type new_span: str or bytes :return: True if the new span can be added to the list, False otherwise. :rtype: bool """ raise NotImplementedError() def encode_span(self, span: Span) -> Union[str, bytes]: """Encodes a single span. :param span: Span object representing the span. :type span: Span :return: encoded span. :rtype: str or bytes """ raise NotImplementedError() def encode_queue(self, queue: List[Union[str, bytes]]) -> Union[str, bytes]: """Encodes a list of pre-encoded spans. :param queue: list of encoded spans. :type queue: list :return: encoded list, type depends on the encoding. :rtype: str or bytes """ raise NotImplementedError() class JSONEndpoint(TypedDict, total=False): serviceName: Optional[str] port: Optional[int] ipv4: Optional[str] ipv6: Optional[str] def _is_str_list(any_str_list: List[Union[str, bytes]]) -> TypeGuard[List[str]]: return all(isinstance(element, str) for element in any_str_list) class _BaseJSONEncoder(IEncoder): """V1 and V2 JSON encoders need many common helper functions""" def fits( self, current_count: int, current_size: int, max_size: int, new_span: Union[str, bytes], ) -> bool: """Checks if the new span fits in the max payload size. Json lists only have a 2 bytes overhead from '[]' plus 1 byte from ',' between elements """ return 2 + current_count + current_size + len(new_span) <= max_size def _create_json_endpoint(self, endpoint: Endpoint, is_v1: bool) -> JSONEndpoint: """Converts an Endpoint to a JSON endpoint dict. :param endpoint: endpoint object to convert. :type endpoint: Endpoint :param is_v1: whether we're serializing a v1 span. This is needed since in v1 some fields default to an empty string rather than being dropped if they're not set. :type is_v1: bool :return: dict representing a JSON endpoint. :rtype: dict """ json_endpoint: JSONEndpoint = {} if endpoint.service_name: json_endpoint["serviceName"] = endpoint.service_name elif is_v1: # serviceName is mandatory in v1 json_endpoint["serviceName"] = "" if endpoint.port and endpoint.port != 0: json_endpoint["port"] = endpoint.port if endpoint.ipv4 is not None: json_endpoint["ipv4"] = endpoint.ipv4 if endpoint.ipv6 is not None: json_endpoint["ipv6"] = endpoint.ipv6 return json_endpoint def encode_queue(self, queue: List[Union[str, bytes]]) -> str: """Concatenates the list to a JSON list""" assert _is_str_list(queue) return "[" + ",".join(queue) + "]" class JSONv1BinaryAnnotation(TypedDict): key: str value: Union[str, bool, None] endpoint: JSONEndpoint class JSONv1Annotation(TypedDict): endpoint: JSONEndpoint timestamp: int value: str class JSONv1Span(TypedDict, total=False): traceId: str name: Optional[str] id: Optional[str] annotations: List[JSONv1Annotation] binaryAnnotations: List[JSONv1BinaryAnnotation] parentId: str timestamp: int duration: int class _V1JSONEncoder(_BaseJSONEncoder): """JSON encoder for V1 spans.""" def encode_remote_endpoint( self, remote_endpoint: Endpoint, kind: Kind, binary_annotations: List[JSONv1BinaryAnnotation], ) -> None: json_remote_endpoint = self._create_json_endpoint(remote_endpoint, True) if kind == Kind.CLIENT: key = "sa" elif kind == Kind.SERVER: key = "ca" binary_annotations.append( {"key": key, "value": True, "endpoint": json_remote_endpoint} ) def encode_span(self, v2_span: Span) -> str: """Encodes a single span to JSON.""" span = v2_span.build_v1_span() json_span: JSONv1Span = { "traceId": span.trace_id, "name": span.name, "id": span.id, "annotations": [], "binaryAnnotations": [], } if span.parent_id: json_span["parentId"] = span.parent_id if span.timestamp: json_span["timestamp"] = int(span.timestamp * 1000000) if span.duration: json_span["duration"] = int(span.duration * 1000000) assert span.endpoint is not None v1_endpoint = self._create_json_endpoint(span.endpoint, True) for key, timestamp in span.annotations.items(): assert timestamp is not None json_span["annotations"].append( { "endpoint": v1_endpoint, "timestamp": int(timestamp * 1000000), "value": key, } ) for key, value in span.binary_annotations.items(): json_span["binaryAnnotations"].append( {"key": key, "value": value, "endpoint": v1_endpoint} ) # Add sa/ca binary annotations if v2_span.remote_endpoint: self.encode_remote_endpoint( v2_span.remote_endpoint, v2_span.kind, json_span["binaryAnnotations"], ) encoded_span = json.dumps(json_span) return encoded_span class JSONv2Annotation(TypedDict): timestamp: int value: str class JSONv2Span(TypedDict, total=False): traceId: str id: Optional[str] name: str parentId: str timestamp: int duration: int shared: bool kind: str localEndpoint: JSONEndpoint remoteEndpoint: JSONEndpoint tags: Dict[str, str] annotations: List[JSONv2Annotation] def _is_dict_str_float( mapping: Dict[str, Optional[float]] ) -> TypeGuard[Dict[str, float]]: return all(isinstance(value, float) for key, value in mapping.items()) class _V2JSONEncoder(_BaseJSONEncoder): """JSON encoder for V2 spans.""" def encode_span(self, span: Span) -> str: """Encodes a single span to JSON.""" if span.span_id: # validate that this is a hex number unsigned_hex_to_signed_int(span.span_id) json_span: JSONv2Span = { "traceId": span.trace_id, "id": span.span_id, } if span.name: json_span["name"] = span.name if span.parent_id: json_span["parentId"] = span.parent_id if span.timestamp: json_span["timestamp"] = int(span.timestamp * 1000000) if span.duration: json_span["duration"] = int(span.duration * 1000000) if span.shared is True: json_span["shared"] = True if span.kind and span.kind.value is not None: json_span["kind"] = span.kind.value if span.local_endpoint: json_span["localEndpoint"] = self._create_json_endpoint( span.local_endpoint, False, ) if span.remote_endpoint: json_span["remoteEndpoint"] = self._create_json_endpoint( span.remote_endpoint, False, ) if span.tags and len(span.tags) > 0: # Ensure that tags are all strings json_span["tags"] = { str(key): str(value) for key, value in span.tags.items() } if span.annotations: assert _is_dict_str_float(span.annotations) json_span["annotations"] = [ {"timestamp": int(timestamp * 1000000), "value": key} for key, timestamp in span.annotations.items() ] encoded_span = json.dumps(json_span) return encoded_span def _is_bytes_list(any_str_list: List[Union[str, bytes]]) -> TypeGuard[List[bytes]]: return all(isinstance(element, bytes) for element in any_str_list) class _V2ProtobufEncoder(IEncoder): """Protobuf encoder for V2 spans.""" def fits( self, current_count: int, current_size: int, max_size: int, new_span: Union[str, bytes], ) -> bool: """Checks if the new span fits in the max payload size.""" return current_size + len(new_span) <= max_size def encode_span(self, span: Span) -> bytes: """Encodes a single span to protobuf.""" if not protobuf.installed(): raise ZipkinError( "protobuf encoding requires installing the protobuf's extra " "requirements. Use py-zipkin[protobuf] in your requirements.txt." ) pb_span = protobuf.create_protobuf_span(span) return protobuf.encode_pb_list([pb_span]) def encode_queue(self, queue: List[Union[str, bytes]]) -> bytes: """Concatenates the list to a protobuf list and encodes it to bytes""" assert _is_bytes_list(queue) return b"".join(queue) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663591.0 py_zipkin-1.2.8/py_zipkin/encoding/_helpers.py0000644000175100001730000001665500000000000022332 0ustar00runnerdocker00000000000000import socket from collections import OrderedDict from typing import Dict from typing import MutableMapping from typing import NamedTuple from typing import Optional from py_zipkin.encoding._types import Kind from py_zipkin.exception import ZipkinError class Endpoint(NamedTuple): service_name: Optional[str] ipv4: Optional[str] ipv6: Optional[str] port: Optional[int] class _V1Span(NamedTuple): trace_id: str name: Optional[str] parent_id: Optional[str] id: Optional[str] timestamp: Optional[float] duration: Optional[float] endpoint: Optional[Endpoint] annotations: MutableMapping[str, Optional[float]] binary_annotations: Dict[str, Optional[str]] remote_endpoint: Optional[Endpoint] class Span: """Internal V2 Span representation.""" def __init__( self, trace_id: str, name: Optional[str], parent_id: Optional[str], span_id: Optional[str], kind: Kind, timestamp: Optional[float], duration: Optional[float], local_endpoint: Optional[Endpoint] = None, remote_endpoint: Optional[Endpoint] = None, debug: bool = False, shared: bool = False, annotations: Optional[Dict[str, Optional[float]]] = None, tags: Optional[Dict[str, Optional[str]]] = None, ): """Creates a new Span. :param trace_id: Trace id. :type trace_id: str :param name: Name of the span. :type name: str :param parent_id: Parent span id. :type parent_id: str :param span_id: Span id. :type span_id: str :param kind: Span type (client, server, local, etc...) :type kind: Kind :param timestamp: start timestamp in seconds. :type timestamp: float :param duration: span duration in seconds. :type duration: float :param local_endpoint: the host that recorded this span. :type local_endpoint: Endpoint :param remote_endpoint: the remote service. :type remote_endpoint: Endpoint :param debug: True is a request to store this span even if it overrides sampling policy. :type debug: bool :param shared: True if we are contributing to a span started by another tracer (ex on a different host). :type shared: bool :param annotations: Optional dict of str -> timestamp annotations. :type annotations: dict :param tags: Optional dict of str -> str span tags. :type tags: dict """ self.trace_id = trace_id self.name = name self.parent_id = parent_id self.span_id = span_id self.kind = kind self.timestamp = timestamp self.duration = duration self.local_endpoint = local_endpoint self.remote_endpoint = remote_endpoint self.debug = debug self.shared = shared self.annotations = annotations or {} self.tags = tags or {} if not isinstance(kind, Kind): raise ZipkinError(f"Invalid kind value {kind}. Must be of type Kind.") if local_endpoint and not isinstance(local_endpoint, Endpoint): raise ZipkinError("Invalid local_endpoint value. Must be of type Endpoint.") if remote_endpoint and not isinstance(remote_endpoint, Endpoint): raise ZipkinError( "Invalid remote_endpoint value. Must be of type Endpoint." ) def __eq__(self, other: object) -> bool: # pragma: no cover """Compare function to help assert span1 == span2 in py3""" return self.__dict__ == other.__dict__ def __cmp__(self, other: "Span") -> int: # pragma: no cover """Compare function to help assert span1 == span2 in py2""" return self.__dict__ == other.__dict__ def __str__(self) -> str: # pragma: no cover """Compare function to nicely print Span rather than just the pointer""" return str(self.__dict__) def build_v1_span(self) -> _V1Span: """Builds and returns a V1 Span. :return: newly generated _V1Span :rtype: _V1Span """ annotations: MutableMapping[str, Optional[float]] = OrderedDict([]) assert self.timestamp is not None if self.kind == Kind.CLIENT: assert self.duration is not None annotations["cs"] = self.timestamp annotations["cr"] = self.timestamp + self.duration elif self.kind == Kind.SERVER: assert self.duration is not None annotations["sr"] = self.timestamp annotations["ss"] = self.timestamp + self.duration elif self.kind == Kind.PRODUCER: annotations["ms"] = self.timestamp elif self.kind == Kind.CONSUMER: annotations["mr"] = self.timestamp # Add user-defined annotations. We write them in annotations # instead of the opposite so that user annotations will override # any automatically generated annotation. annotations.update(self.annotations) return _V1Span( trace_id=self.trace_id, name=self.name, parent_id=self.parent_id, id=self.span_id, timestamp=self.timestamp if self.shared is False else None, duration=self.duration if self.shared is False else None, endpoint=self.local_endpoint, annotations=annotations, binary_annotations=self.tags, remote_endpoint=self.remote_endpoint, ) def create_endpoint( port: Optional[int] = None, service_name: Optional[str] = None, host: Optional[str] = None, use_defaults: bool = True, ) -> Endpoint: """Creates a new Endpoint object. :param port: TCP/UDP port. Defaults to 0. :type port: int :param service_name: service name as a str. Defaults to 'unknown'. :type service_name: str :param host: ipv4 or ipv6 address of the host. Defaults to the current host ip. :type host: str :param use_defaults: whether to use defaults. :type use_defaults: bool :returns: zipkin Endpoint object """ if use_defaults: if port is None: port = 0 if service_name is None: service_name = "unknown" if host is None: try: host = socket.gethostbyname(socket.gethostname()) except socket.gaierror: host = "127.0.0.1" ipv4 = None ipv6 = None if host: # Check ipv4 or ipv6. try: socket.inet_pton(socket.AF_INET, host) ipv4 = host except OSError: # If it's not an ipv4 address, maybe it's ipv6. try: socket.inet_pton(socket.AF_INET6, host) ipv6 = host except OSError: # If it's neither ipv4 or ipv6, leave both ip addresses unset. pass return Endpoint(ipv4=ipv4, ipv6=ipv6, port=port, service_name=service_name) def copy_endpoint_with_new_service_name( endpoint: Endpoint, new_service_name: Optional[str], ) -> Endpoint: """Creates a copy of a given endpoint with a new service name. :param endpoint: existing Endpoint object :type endpoint: Endpoint :param new_service_name: new service name :type new_service_name: str :returns: zipkin new Endpoint object """ return Endpoint( service_name=new_service_name, ipv4=endpoint.ipv4, ipv6=endpoint.ipv6, port=endpoint.port, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663591.0 py_zipkin-1.2.8/py_zipkin/encoding/_types.py0000644000175100001730000000055000000000000022017 0ustar00runnerdocker00000000000000from enum import Enum class Encoding(Enum): """Output encodings.""" V1_THRIFT = "V1_THRIFT" # No longer supported V1_JSON = "V1_JSON" V2_JSON = "V2_JSON" V2_PROTO3 = "V2_PROTO3" class Kind(Enum): """Type of Span.""" CLIENT = "CLIENT" SERVER = "SERVER" PRODUCER = "PRODUCER" CONSUMER = "CONSUMER" LOCAL = None ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1679663596.5611188 py_zipkin-1.2.8/py_zipkin/encoding/protobuf/0000755000175100001730000000000000000000000022002 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663591.0 py_zipkin-1.2.8/py_zipkin/encoding/protobuf/__init__.py0000644000175100001730000001423700000000000024122 0ustar00runnerdocker00000000000000import socket import struct from typing import Dict from typing import List from typing import Optional from typing_extensions import TypedDict from typing_extensions import TypeGuard from py_zipkin.encoding._helpers import Endpoint from py_zipkin.encoding._helpers import Span from py_zipkin.encoding._types import Kind from py_zipkin.util import unsigned_hex_to_signed_int try: from py_zipkin.encoding.protobuf import zipkin_pb2 except ImportError: # pragma: no cover pass def installed() -> bool: # pragma: no cover """Checks whether the protobud library is installed and can be used. :return: True if everything's fine, False otherwise :rtype: bool """ try: _ = zipkin_pb2 return True except NameError: return False def encode_pb_list(pb_spans: "List[zipkin_pb2.Span]") -> bytes: """Encode list of protobuf Spans to binary. :param pb_spans: list of protobuf Spans. :type pb_spans: list of zipkin_pb2.Span :return: encoded list. :rtype: bytes """ pb_list = zipkin_pb2.ListOfSpans() pb_list.spans.extend(pb_spans) return pb_list.SerializeToString() class ProtobufSpanArgsDict(TypedDict, total=False): trace_id: bytes parent_id: bytes id: bytes kind: "zipkin_pb2.Span._Kind.ValueType" name: str timestamp: int duration: int local_endpoint: "zipkin_pb2.Endpoint" remote_endpoint: "zipkin_pb2.Endpoint" annotations: "List[zipkin_pb2.Annotation]" tags: Dict[str, str] debug: bool shared: bool def _is_dict_str_str(mapping: Dict[str, Optional[str]]) -> TypeGuard[Dict[str, str]]: return all(isinstance(value, str) for _, value in mapping.items()) def create_protobuf_span(span: Span) -> "zipkin_pb2.Span": """Converts a py_zipkin Span in a protobuf Span. :param span: py_zipkin Span to convert. :type span: py_zipkin.encoding.Span :return: protobuf's Span :rtype: zipkin_pb2.Span """ # Protobuf's composite types (i.e. Span's local_endpoint) are immutable. # So we can't create a zipkin_pb2.Span here and then set the appropriate # fields since `pb_span.local_endpoint = zipkin_pb2.Endpoint` fails. # Instead we just create the kwargs and pass them in to the Span constructor. pb_kwargs: ProtobufSpanArgsDict = {} pb_kwargs["trace_id"] = _hex_to_bytes(span.trace_id) if span.parent_id: pb_kwargs["parent_id"] = _hex_to_bytes(span.parent_id) assert span.span_id is not None pb_kwargs["id"] = _hex_to_bytes(span.span_id) pb_kind = _get_protobuf_kind(span.kind) if pb_kind: pb_kwargs["kind"] = pb_kind if span.name: pb_kwargs["name"] = span.name if span.timestamp: pb_kwargs["timestamp"] = int(span.timestamp * 1000 * 1000) if span.duration: pb_kwargs["duration"] = int(span.duration * 1000 * 1000) if span.local_endpoint: pb_kwargs["local_endpoint"] = _convert_endpoint(span.local_endpoint) if span.remote_endpoint: pb_kwargs["remote_endpoint"] = _convert_endpoint(span.remote_endpoint) if len(span.annotations) > 0: pb_kwargs["annotations"] = _convert_annotations(span.annotations) if len(span.tags) > 0: assert _is_dict_str_str(span.tags) pb_kwargs["tags"] = span.tags if span.debug: pb_kwargs["debug"] = span.debug if span.shared: pb_kwargs["shared"] = span.shared return zipkin_pb2.Span(**pb_kwargs) def _hex_to_bytes(hex_id: str) -> bytes: """Encodes to hexadecimal ids to big-endian binary. :param hex_id: hexadecimal id to encode. :type hex_id: str :return: binary representation. :type: bytes """ if len(hex_id) <= 16: int_id = unsigned_hex_to_signed_int(hex_id) return struct.pack(">q", int_id) else: # There's no 16-bytes encoding in Python's struct. So we convert the # id as 2 64 bit ids and then concatenate the result. # NOTE: we count 16 chars from the right (:-16) rather than the left so # that ids with less than 32 chars will be correctly pre-padded with 0s. high_id = unsigned_hex_to_signed_int(hex_id[:-16]) high_bin = struct.pack(">q", high_id) low_id = unsigned_hex_to_signed_int(hex_id[-16:]) low_bin = struct.pack(">q", low_id) return high_bin + low_bin def _get_protobuf_kind(kind: Kind) -> "Optional[zipkin_pb2.Span._Kind.ValueType]": """Converts py_zipkin's Kind to Protobuf's Kind. :param kind: py_zipkin's Kind. :type kind: py_zipkin.Kind :return: correcponding protobuf's kind value. :rtype: zipkin_pb2.Span._Kind.ValueType """ if kind == Kind.CLIENT: return zipkin_pb2.Span.CLIENT elif kind == Kind.SERVER: return zipkin_pb2.Span.SERVER elif kind == Kind.PRODUCER: return zipkin_pb2.Span.PRODUCER elif kind == Kind.CONSUMER: return zipkin_pb2.Span.CONSUMER return None def _convert_endpoint(endpoint: Endpoint) -> "zipkin_pb2.Endpoint": """Converts py_zipkin's Endpoint to Protobuf's Endpoint. :param endpoint: py_zipkins' endpoint to convert. :type endpoint: py_zipkin.encoding.Endpoint :return: corresponding protobuf's endpoint. :rtype: zipkin_pb2.Endpoint """ pb_endpoint = zipkin_pb2.Endpoint() if endpoint.service_name: pb_endpoint.service_name = endpoint.service_name if endpoint.port and endpoint.port != 0: pb_endpoint.port = endpoint.port if endpoint.ipv4: pb_endpoint.ipv4 = socket.inet_pton(socket.AF_INET, endpoint.ipv4) if endpoint.ipv6: pb_endpoint.ipv6 = socket.inet_pton(socket.AF_INET6, endpoint.ipv6) return pb_endpoint def _convert_annotations( annotations: Dict[str, Optional[float]] ) -> "List[zipkin_pb2.Annotation]": """Converts py_zipkin's annotations dict to protobuf. :param annotations: annotations dict. :type annotations: dict :return: corresponding protobuf's list of annotations. :rtype: list """ pb_annotations = [] for value, ts in annotations.items(): assert ts is not None pb_annotations.append( zipkin_pb2.Annotation(timestamp=int(ts * 1000 * 1000), value=value) ) return pb_annotations ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663591.0 py_zipkin-1.2.8/py_zipkin/encoding/protobuf/zipkin_pb2.py0000644000175100001730000001206400000000000024426 0ustar00runnerdocker00000000000000# -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: py_zipkin/encoding/protobuf/zipkin.proto """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import message as _message from google.protobuf import reflection as _reflection from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n(py_zipkin/encoding/protobuf/zipkin.proto\x12\rzipkin.proto3\"\xf5\x03\n\x04Span\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x11\n\tparent_id\x18\x02 \x01(\x0c\x12\n\n\x02id\x18\x03 \x01(\x0c\x12&\n\x04kind\x18\x04 \x01(\x0e\x32\x18.zipkin.proto3.Span.Kind\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x11\n\ttimestamp\x18\x06 \x01(\x06\x12\x10\n\x08\x64uration\x18\x07 \x01(\x04\x12/\n\x0elocal_endpoint\x18\x08 \x01(\x0b\x32\x17.zipkin.proto3.Endpoint\x12\x30\n\x0fremote_endpoint\x18\t \x01(\x0b\x32\x17.zipkin.proto3.Endpoint\x12.\n\x0b\x61nnotations\x18\n \x03(\x0b\x32\x19.zipkin.proto3.Annotation\x12+\n\x04tags\x18\x0b \x03(\x0b\x32\x1d.zipkin.proto3.Span.TagsEntry\x12\r\n\x05\x64\x65\x62ug\x18\x0c \x01(\x08\x12\x0e\n\x06shared\x18\r \x01(\x08\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"U\n\x04Kind\x12\x19\n\x15SPAN_KIND_UNSPECIFIED\x10\x00\x12\n\n\x06\x43LIENT\x10\x01\x12\n\n\x06SERVER\x10\x02\x12\x0c\n\x08PRODUCER\x10\x03\x12\x0c\n\x08\x43ONSUMER\x10\x04\"J\n\x08\x45ndpoint\x12\x14\n\x0cservice_name\x18\x01 \x01(\t\x12\x0c\n\x04ipv4\x18\x02 \x01(\x0c\x12\x0c\n\x04ipv6\x18\x03 \x01(\x0c\x12\x0c\n\x04port\x18\x04 \x01(\x05\".\n\nAnnotation\x12\x11\n\ttimestamp\x18\x01 \x01(\x06\x12\r\n\x05value\x18\x02 \x01(\t\"1\n\x0bListOfSpans\x12\"\n\x05spans\x18\x01 \x03(\x0b\x32\x13.zipkin.proto3.Span\"\x10\n\x0eReportResponse2T\n\x0bSpanService\x12\x45\n\x06Report\x12\x1a.zipkin.proto3.ListOfSpans\x1a\x1d.zipkin.proto3.ReportResponse\"\x00\x42\x12\n\x0ezipkin2.proto3P\x01\x62\x06proto3') _SPAN = DESCRIPTOR.message_types_by_name['Span'] _SPAN_TAGSENTRY = _SPAN.nested_types_by_name['TagsEntry'] _ENDPOINT = DESCRIPTOR.message_types_by_name['Endpoint'] _ANNOTATION = DESCRIPTOR.message_types_by_name['Annotation'] _LISTOFSPANS = DESCRIPTOR.message_types_by_name['ListOfSpans'] _REPORTRESPONSE = DESCRIPTOR.message_types_by_name['ReportResponse'] _SPAN_KIND = _SPAN.enum_types_by_name['Kind'] Span = _reflection.GeneratedProtocolMessageType('Span', (_message.Message,), { 'TagsEntry' : _reflection.GeneratedProtocolMessageType('TagsEntry', (_message.Message,), { 'DESCRIPTOR' : _SPAN_TAGSENTRY, '__module__' : 'py_zipkin.encoding.protobuf.zipkin_pb2' # @@protoc_insertion_point(class_scope:zipkin.proto3.Span.TagsEntry) }) , 'DESCRIPTOR' : _SPAN, '__module__' : 'py_zipkin.encoding.protobuf.zipkin_pb2' # @@protoc_insertion_point(class_scope:zipkin.proto3.Span) }) _sym_db.RegisterMessage(Span) _sym_db.RegisterMessage(Span.TagsEntry) Endpoint = _reflection.GeneratedProtocolMessageType('Endpoint', (_message.Message,), { 'DESCRIPTOR' : _ENDPOINT, '__module__' : 'py_zipkin.encoding.protobuf.zipkin_pb2' # @@protoc_insertion_point(class_scope:zipkin.proto3.Endpoint) }) _sym_db.RegisterMessage(Endpoint) Annotation = _reflection.GeneratedProtocolMessageType('Annotation', (_message.Message,), { 'DESCRIPTOR' : _ANNOTATION, '__module__' : 'py_zipkin.encoding.protobuf.zipkin_pb2' # @@protoc_insertion_point(class_scope:zipkin.proto3.Annotation) }) _sym_db.RegisterMessage(Annotation) ListOfSpans = _reflection.GeneratedProtocolMessageType('ListOfSpans', (_message.Message,), { 'DESCRIPTOR' : _LISTOFSPANS, '__module__' : 'py_zipkin.encoding.protobuf.zipkin_pb2' # @@protoc_insertion_point(class_scope:zipkin.proto3.ListOfSpans) }) _sym_db.RegisterMessage(ListOfSpans) ReportResponse = _reflection.GeneratedProtocolMessageType('ReportResponse', (_message.Message,), { 'DESCRIPTOR' : _REPORTRESPONSE, '__module__' : 'py_zipkin.encoding.protobuf.zipkin_pb2' # @@protoc_insertion_point(class_scope:zipkin.proto3.ReportResponse) }) _sym_db.RegisterMessage(ReportResponse) _SPANSERVICE = DESCRIPTOR.services_by_name['SpanService'] if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'\n\016zipkin2.proto3P\001' _SPAN_TAGSENTRY._options = None _SPAN_TAGSENTRY._serialized_options = b'8\001' _SPAN._serialized_start=60 _SPAN._serialized_end=561 _SPAN_TAGSENTRY._serialized_start=431 _SPAN_TAGSENTRY._serialized_end=474 _SPAN_KIND._serialized_start=476 _SPAN_KIND._serialized_end=561 _ENDPOINT._serialized_start=563 _ENDPOINT._serialized_end=637 _ANNOTATION._serialized_start=639 _ANNOTATION._serialized_end=685 _LISTOFSPANS._serialized_start=687 _LISTOFSPANS._serialized_end=736 _REPORTRESPONSE._serialized_start=738 _REPORTRESPONSE._serialized_end=754 _SPANSERVICE._serialized_start=756 _SPANSERVICE._serialized_end=840 # @@protoc_insertion_point(module_scope) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663591.0 py_zipkin-1.2.8/py_zipkin/encoding/protobuf/zipkin_pb2.pyi0000644000175100001730000003765100000000000024610 0ustar00runnerdocker00000000000000""" @generated by mypy-protobuf. Do not edit manually! isort:skip_file Copyright 2018-2019 The OpenZipkin 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 builtins import collections.abc import google.protobuf.descriptor import google.protobuf.internal.containers import google.protobuf.internal.enum_type_wrapper import google.protobuf.message import sys import typing if sys.version_info >= (3, 10): import typing as typing_extensions else: import typing_extensions DESCRIPTOR: google.protobuf.descriptor.FileDescriptor class Span(google.protobuf.message.Message): """A span is a single-host view of an operation. A trace is a series of spans (often RPC calls) which nest to form a latency tree. Spans are in the same trace when they share the same trace ID. The parent_id field establishes the position of one span in the tree. The root span is where parent_id is Absent and usually has the longest duration in the trace. However, nested asynchronous work can materialize as child spans whose duration exceed the root span. Spans usually represent remote activity such as RPC calls, or messaging producers and consumers. However, they can also represent in-process activity in any position of the trace. For example, a root span could represent a server receiving an initial client request. A root span could also represent a scheduled job that has no remote context. Encoding notes: Epoch timestamp are encoded fixed64 as varint would also be 8 bytes, and more expensive to encode and size. Duration is stored uint64, as often the numbers are quite small. Default values are ok, as only natural numbers are used. For example, zero is an invalid timestamp and an invalid duration, false values for debug or shared are ignorable, and zero-length strings also coerce to null. The next id is 14. Note fields up to 15 take 1 byte to encode. Take care when adding new fields https://developers.google.com/protocol-buffers/docs/proto3#assigning-tags """ DESCRIPTOR: google.protobuf.descriptor.Descriptor class _Kind: ValueType = typing.NewType("ValueType", builtins.int) V: typing_extensions.TypeAlias = ValueType class _KindEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[Span._Kind.ValueType], builtins.type): # noqa: F821 DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor SPAN_KIND_UNSPECIFIED: Span._Kind.ValueType # 0 """Default value interpreted as absent.""" CLIENT: Span._Kind.ValueType # 1 """The span represents the client side of an RPC operation, implying the following: timestamp is the moment a request was sent to the server. duration is the delay until a response or an error was received. remote_endpoint is the server. """ SERVER: Span._Kind.ValueType # 2 """The span represents the server side of an RPC operation, implying the following: timestamp is the moment a client request was received. duration is the delay until a response was sent or an error. remote_endpoint is the client. """ PRODUCER: Span._Kind.ValueType # 3 """The span represents production of a message to a remote broker, implying the following: timestamp is the moment a message was sent to a destination. duration is the delay sending the message, such as batching. remote_endpoint is the broker. """ CONSUMER: Span._Kind.ValueType # 4 """The span represents consumption of a message from a remote broker, not time spent servicing it. For example, a message processor would be an in-process child span of a consumer. Consumer spans imply the following: timestamp is the moment a message was received from an origin. duration is the delay consuming the message, such as from backlog. remote_endpoint is the broker. """ class Kind(_Kind, metaclass=_KindEnumTypeWrapper): """When present, kind clarifies timestamp, duration and remote_endpoint. When absent, the span is local or incomplete. Unlike client and server, there is no direct critical path latency relationship between producer and consumer spans. """ SPAN_KIND_UNSPECIFIED: Span.Kind.ValueType # 0 """Default value interpreted as absent.""" CLIENT: Span.Kind.ValueType # 1 """The span represents the client side of an RPC operation, implying the following: timestamp is the moment a request was sent to the server. duration is the delay until a response or an error was received. remote_endpoint is the server. """ SERVER: Span.Kind.ValueType # 2 """The span represents the server side of an RPC operation, implying the following: timestamp is the moment a client request was received. duration is the delay until a response was sent or an error. remote_endpoint is the client. """ PRODUCER: Span.Kind.ValueType # 3 """The span represents production of a message to a remote broker, implying the following: timestamp is the moment a message was sent to a destination. duration is the delay sending the message, such as batching. remote_endpoint is the broker. """ CONSUMER: Span.Kind.ValueType # 4 """The span represents consumption of a message from a remote broker, not time spent servicing it. For example, a message processor would be an in-process child span of a consumer. Consumer spans imply the following: timestamp is the moment a message was received from an origin. duration is the delay consuming the message, such as from backlog. remote_endpoint is the broker. """ class TagsEntry(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor KEY_FIELD_NUMBER: builtins.int VALUE_FIELD_NUMBER: builtins.int key: builtins.str value: builtins.str def __init__( self, *, key: builtins.str = ..., value: builtins.str = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... TRACE_ID_FIELD_NUMBER: builtins.int PARENT_ID_FIELD_NUMBER: builtins.int ID_FIELD_NUMBER: builtins.int KIND_FIELD_NUMBER: builtins.int NAME_FIELD_NUMBER: builtins.int TIMESTAMP_FIELD_NUMBER: builtins.int DURATION_FIELD_NUMBER: builtins.int LOCAL_ENDPOINT_FIELD_NUMBER: builtins.int REMOTE_ENDPOINT_FIELD_NUMBER: builtins.int ANNOTATIONS_FIELD_NUMBER: builtins.int TAGS_FIELD_NUMBER: builtins.int DEBUG_FIELD_NUMBER: builtins.int SHARED_FIELD_NUMBER: builtins.int trace_id: builtins.bytes """Randomly generated, unique identifier for a trace, set on all spans within it. This field is required and encoded as 8 or 16 bytes, in big endian byte order. """ parent_id: builtins.bytes """The parent span ID or absent if this the root span in a trace.""" id: builtins.bytes """Unique identifier for this operation within the trace. This field is required and encoded as 8 opaque bytes. """ kind: global___Span.Kind.ValueType """When present, used to interpret remote_endpoint""" name: builtins.str """The logical operation this span represents in lowercase (e.g. rpc method). Leave absent if unknown. As these are lookup labels, take care to ensure names are low cardinality. For example, do not embed variables into the name. """ timestamp: builtins.int """Epoch microseconds of the start of this span, possibly absent if incomplete. For example, 1502787600000000 corresponds to 2017-08-15 09:00 UTC This value should be set directly by instrumentation, using the most precise value possible. For example, gettimeofday or multiplying epoch millis by 1000. There are three known edge-cases where this could be reported absent. - A span was allocated but never started (ex not yet received a timestamp) - The span's start event was lost - Data about a completed span (ex tags) were sent after the fact """ duration: builtins.int """Duration in microseconds of the critical path, if known. Durations of less than one are rounded up. Duration of children can be longer than their parents due to asynchronous operations. For example 150 milliseconds is 150000 microseconds. """ @property def local_endpoint(self) -> global___Endpoint: """The host that recorded this span, primarily for query by service name. Instrumentation should always record this. Usually, absent implies late data. The IP address corresponding to this is usually the site local or advertised service address. When present, the port indicates the listen port. """ @property def remote_endpoint(self) -> global___Endpoint: """When an RPC (or messaging) span, indicates the other side of the connection. By recording the remote endpoint, your trace will contain network context even if the peer is not tracing. For example, you can record the IP from the "X-Forwarded-For" header or the service name and socket of a remote peer. """ @property def annotations(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Annotation]: """Associates events that explain latency with the time they happened.""" @property def tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: """Tags give your span context for search, viewing and analysis. For example, a key "your_app.version" would let you lookup traces by version. A tag "sql.query" isn't searchable, but it can help in debugging when viewing a trace. """ debug: builtins.bool """True is a request to store this span even if it overrides sampling policy. This is true when the "X-B3-Flags" header has a value of 1. """ shared: builtins.bool """True if we are contributing to a span started by another tracer (ex on a different host). """ def __init__( self, *, trace_id: builtins.bytes = ..., parent_id: builtins.bytes = ..., id: builtins.bytes = ..., kind: global___Span.Kind.ValueType = ..., name: builtins.str = ..., timestamp: builtins.int = ..., duration: builtins.int = ..., local_endpoint: global___Endpoint | None = ..., remote_endpoint: global___Endpoint | None = ..., annotations: collections.abc.Iterable[global___Annotation] | None = ..., tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., debug: builtins.bool = ..., shared: builtins.bool = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["local_endpoint", b"local_endpoint", "remote_endpoint", b"remote_endpoint"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["annotations", b"annotations", "debug", b"debug", "duration", b"duration", "id", b"id", "kind", b"kind", "local_endpoint", b"local_endpoint", "name", b"name", "parent_id", b"parent_id", "remote_endpoint", b"remote_endpoint", "shared", b"shared", "tags", b"tags", "timestamp", b"timestamp", "trace_id", b"trace_id"]) -> None: ... global___Span = Span class Endpoint(google.protobuf.message.Message): """The network context of a node in the service graph. The next id is 5. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor SERVICE_NAME_FIELD_NUMBER: builtins.int IPV4_FIELD_NUMBER: builtins.int IPV6_FIELD_NUMBER: builtins.int PORT_FIELD_NUMBER: builtins.int service_name: builtins.str """Lower-case label of this node in the service graph, such as "favstar". Leave absent if unknown. This is a primary label for trace lookup and aggregation, so it should be intuitive and consistent. Many use a name from service discovery. """ ipv4: builtins.bytes """4 byte representation of the primary IPv4 address associated with this connection. Absent if unknown. """ ipv6: builtins.bytes """16 byte representation of the primary IPv6 address associated with this connection. Absent if unknown. Prefer using the ipv4 field for mapped addresses. """ port: builtins.int """Depending on context, this could be a listen port or the client-side of a socket. Absent if unknown. """ def __init__( self, *, service_name: builtins.str = ..., ipv4: builtins.bytes = ..., ipv6: builtins.bytes = ..., port: builtins.int = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["ipv4", b"ipv4", "ipv6", b"ipv6", "port", b"port", "service_name", b"service_name"]) -> None: ... global___Endpoint = Endpoint class Annotation(google.protobuf.message.Message): """Associates an event that explains latency with a timestamp. Unlike log statements, annotations are often codes. Ex. "ws" for WireSend The next id is 3. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor TIMESTAMP_FIELD_NUMBER: builtins.int VALUE_FIELD_NUMBER: builtins.int timestamp: builtins.int """Epoch microseconds of this event. For example, 1502787600000000 corresponds to 2017-08-15 09:00 UTC This value should be set directly by instrumentation, using the most precise value possible. For example, gettimeofday or multiplying epoch millis by 1000. """ value: builtins.str """Usually a short tag indicating an event, like "error" While possible to add larger data, such as garbage collection details, low cardinality event names both keep the size of spans down and also are easy to search against. """ def __init__( self, *, timestamp: builtins.int = ..., value: builtins.str = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["timestamp", b"timestamp", "value", b"value"]) -> None: ... global___Annotation = Annotation class ListOfSpans(google.protobuf.message.Message): """A list of spans with possibly different trace ids, in no particular order. This is used for all transports: POST, Kafka messages etc. No other fields are expected, This message facilitates the mechanics of encoding a list, as a field number is required. The name of this type is the same in the OpenApi aka Swagger specification. https://zipkin.io/zipkin-api/#/default/post_spans """ DESCRIPTOR: google.protobuf.descriptor.Descriptor SPANS_FIELD_NUMBER: builtins.int @property def spans(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Span]: ... def __init__( self, *, spans: collections.abc.Iterable[global___Span] | None = ..., ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["spans", b"spans"]) -> None: ... global___ListOfSpans = ListOfSpans class ReportResponse(google.protobuf.message.Message): """Response for SpanService/Report RPC. This response currently does not return any information beyond indicating that the request has finished. That said, it may be extended in the future. """ DESCRIPTOR: google.protobuf.descriptor.Descriptor def __init__( self, ) -> None: ... global___ReportResponse = ReportResponse ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663591.0 py_zipkin-1.2.8/py_zipkin/exception.py0000644000175100001730000000013000000000000020716 0ustar00runnerdocker00000000000000class ZipkinError(Exception): """Custom error to be raised on Zipkin exceptions.""" ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1679663596.5611188 py_zipkin-1.2.8/py_zipkin/instrumentations/0000755000175100001730000000000000000000000022002 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663591.0 py_zipkin-1.2.8/py_zipkin/instrumentations/__init__.py0000644000175100001730000000000000000000000024101 0ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663591.0 py_zipkin-1.2.8/py_zipkin/instrumentations/python_threads.py0000644000175100001730000000304500000000000025411 0ustar00runnerdocker00000000000000import threading from functools import partial from typing import Any from typing import Callable from py_zipkin import storage _orig_Thread_start = threading.Thread.start _orig_Thread_run = threading.Thread.run def _Thread_pre_start(self: Any) -> None: self._orig_tracer = None if storage.has_default_tracer(): self._orig_tracer = storage.get_default_tracer().copy() def _Thread_wrap_run(self: Any, actual_run_fn: Callable[[], None]) -> None: # This executes in the new OS thread if self._orig_tracer: # Inject our copied Tracer into our thread-local-storage storage.set_default_tracer(self._orig_tracer) try: actual_run_fn() finally: # I think this is probably a good idea for the same reasons the # parent class deletes __target, __args, and __kwargs if self._orig_tracer: del self._orig_tracer def patch_threading() -> None: # pragma: no cover """Monkey-patch threading module to work better with tracing.""" def _new_start(self: Any) -> None: _Thread_pre_start(self) _orig_Thread_start(self) def _new_run(self: Any) -> None: _Thread_wrap_run(self, partial(_orig_Thread_run, self)) threading.Thread.start = _new_start # type: ignore[method-assign] threading.Thread.run = _new_run # type: ignore[method-assign] def unpatch_threading() -> None: # pragma: no cover threading.Thread.start = _orig_Thread_start # type: ignore[method-assign] threading.Thread.run = _orig_Thread_run # type: ignore[method-assign] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663591.0 py_zipkin-1.2.8/py_zipkin/logging_helper.py0000644000175100001730000001762000000000000021721 0ustar00runnerdocker00000000000000import os import time from types import TracebackType from typing import Callable from typing import Dict from typing import List from typing import Optional from typing import Type from typing import Union from py_zipkin import Kind from py_zipkin.encoding._encoders import get_encoder from py_zipkin.encoding._encoders import IEncoder from py_zipkin.encoding._helpers import copy_endpoint_with_new_service_name from py_zipkin.encoding._helpers import Endpoint from py_zipkin.encoding._helpers import Span from py_zipkin.encoding._types import Encoding from py_zipkin.exception import ZipkinError from py_zipkin.storage import Tracer from py_zipkin.transport import BaseTransportHandler from py_zipkin.util import ZipkinAttrs LOGGING_END_KEY = "py_zipkin.logging_end" TransportHandler = Union[BaseTransportHandler, Callable[[Union[str, bytes]], None]] class ZipkinLoggingContext: """A logging context specific to a Zipkin trace. If the trace is sampled, the logging context sends serialized Zipkin spans to a transport_handler. The logging context sends root "server" or "client" span, as well as all local child spans collected within this context. This class should only be used by the main `zipkin_span` entrypoint. """ def __init__( self, zipkin_attrs: ZipkinAttrs, endpoint: Endpoint, span_name: str, transport_handler: Optional[TransportHandler], report_root_timestamp: float, get_tracer: Callable[[], Tracer], service_name: str, binary_annotations: Optional[Dict[str, Optional[str]]] = None, add_logging_annotation: bool = False, client_context: bool = False, max_span_batch_size: Optional[int] = None, firehose_handler: Optional[TransportHandler] = None, encoding: Optional[Encoding] = None, annotations: Optional[Dict[str, Optional[float]]] = None, ): self.zipkin_attrs = zipkin_attrs self.endpoint = endpoint self.span_name = span_name self.transport_handler = transport_handler self.response_status_code = 0 self._get_tracer = get_tracer self.service_name = service_name self.report_root_timestamp = report_root_timestamp self.tags = binary_annotations or {} self.add_logging_annotation = add_logging_annotation self.client_context = client_context self.max_span_batch_size = max_span_batch_size self.firehose_handler = firehose_handler self.annotations = annotations or {} self.remote_endpoint: Optional[Endpoint] = None assert encoding is not None self.encoder = get_encoder(encoding) def start(self) -> "ZipkinLoggingContext": """Actions to be taken before request is handled.""" # Record the start timestamp. self.start_timestamp = time.time() return self def stop(self) -> None: """Actions to be taken post request handling.""" self.emit_spans() def emit_spans(self) -> None: """Main function to log all the annotations stored during the entire request. This is done if the request is sampled and the response was a success. It also logs the service (`ss` and `sr`) or the client ('cs' and 'cr') annotations. """ # FIXME: Should have a single aggregate handler if self.firehose_handler: # FIXME: We need to allow different batching settings per handler self._emit_spans_with_span_sender( ZipkinBatchSender( self.firehose_handler, self.max_span_batch_size, self.encoder ) ) if not self.zipkin_attrs.is_sampled: self._get_tracer().clear() return span_sender = ZipkinBatchSender( self.transport_handler, self.max_span_batch_size, self.encoder ) self._emit_spans_with_span_sender(span_sender) self._get_tracer().clear() def _emit_spans_with_span_sender(self, span_sender: "ZipkinBatchSender") -> None: with span_sender: end_timestamp = time.time() # Collect, annotate, and log client spans from the logging handler for span in self._get_tracer()._span_storage: assert span.local_endpoint is not None span.local_endpoint = copy_endpoint_with_new_service_name( self.endpoint, span.local_endpoint.service_name, ) span_sender.add_span(span) if self.add_logging_annotation: self.annotations[LOGGING_END_KEY] = time.time() span_sender.add_span( Span( trace_id=self.zipkin_attrs.trace_id, name=self.span_name, parent_id=self.zipkin_attrs.parent_span_id, span_id=self.zipkin_attrs.span_id, kind=Kind.CLIENT if self.client_context else Kind.SERVER, timestamp=self.start_timestamp, duration=end_timestamp - self.start_timestamp, local_endpoint=self.endpoint, remote_endpoint=self.remote_endpoint, shared=not self.report_root_timestamp, annotations=self.annotations, tags=self.tags, ) ) class ZipkinBatchSender: MAX_PORTION_SIZE = 100 def __init__( self, transport_handler: Optional[TransportHandler], max_portion_size: Optional[int], encoder: IEncoder, ) -> None: self.transport_handler = transport_handler self.max_portion_size = max_portion_size or self.MAX_PORTION_SIZE self.encoder = encoder if isinstance(self.transport_handler, BaseTransportHandler): self.max_payload_bytes = self.transport_handler.get_max_payload_bytes() else: self.max_payload_bytes = None def __enter__(self) -> "ZipkinBatchSender": self._reset_queue() return self def __exit__( self, _exc_type: Optional[Type[BaseException]], _exc_value: Optional[BaseException], _exc_traceback: Optional[TracebackType], ) -> None: if any((_exc_type, _exc_value, _exc_traceback)): assert _exc_type is not None assert _exc_value is not None assert _exc_traceback is not None filename = os.path.split(_exc_traceback.tb_frame.f_code.co_filename)[1] error = "({}:{}) {}: {}".format( filename, _exc_traceback.tb_lineno, _exc_type.__name__, _exc_value, ) raise ZipkinError(error) else: self.flush() def _reset_queue(self) -> None: self.queue: List[Union[str, bytes]] = [] self.current_size = 0 def add_span(self, internal_span: Span) -> None: encoded_span = self.encoder.encode_span(internal_span) # If we've already reached the max batch size or the new span doesn't # fit in max_payload_bytes, send what we've collected until now and # start a new batch. is_over_size_limit = ( self.max_payload_bytes is not None and not self.encoder.fits( current_count=len(self.queue), current_size=self.current_size, max_size=self.max_payload_bytes, new_span=encoded_span, ) ) is_over_portion_limit = len(self.queue) >= self.max_portion_size if is_over_size_limit or is_over_portion_limit: self.flush() self.queue.append(encoded_span) self.current_size += len(encoded_span) def flush(self) -> None: if self.transport_handler and len(self.queue) > 0: message = self.encoder.encode_queue(self.queue) self.transport_handler(message) self._reset_queue() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663591.0 py_zipkin-1.2.8/py_zipkin/py.typed0000644000175100001730000000000000000000000020041 0ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663591.0 py_zipkin-1.2.8/py_zipkin/request_helpers.py0000644000175100001730000001570500000000000022150 0ustar00runnerdocker00000000000000import logging from typing import Dict from typing import Optional from typing_extensions import TypedDict from py_zipkin.storage import get_default_tracer from py_zipkin.storage import Stack from py_zipkin.storage import Tracer from py_zipkin.util import _should_sample from py_zipkin.util import create_attrs_for_span from py_zipkin.util import generate_random_64bit_string from py_zipkin.util import ZipkinAttrs log = logging.getLogger(__name__) class B3JSON(TypedDict): trace_id: Optional[str] span_id: Optional[str] parent_span_id: Optional[str] sampled_str: Optional[str] def _parse_single_header(b3_header: str) -> B3JSON: """ Parse out and return the data necessary for generating ZipkinAttrs. Returns a dict with the following keys: 'trace_id': str or None 'span_id': str or None 'parent_span_id': str or None 'sampled_str': '0', '1', 'd', or None (defer) """ parsed: B3JSON = { "trace_id": None, "span_id": None, "parent_span_id": None, "sampled_str": None, } # b3={TraceId}-{SpanId}-{SamplingState}-{ParentSpanId} # (last 2 fields optional) # OR # b3={SamplingState} bits = b3_header.split("-") # Handle the lone-sampling-decision case: if len(bits) == 1: if bits[0] in ("0", "1", "d"): parsed["sampled_str"] = bits[0] return parsed raise ValueError("Invalid sample-only value: %r" % bits[0]) if len(bits) > 4: # Too many segments raise ValueError("Too many segments in b3 header: %r" % b3_header) parsed["trace_id"] = bits[0] if not parsed["trace_id"]: raise ValueError("Bad or missing TraceId") parsed["span_id"] = bits[1] if not parsed["span_id"]: raise ValueError("Bad or missing SpanId") if len(bits) > 3: parsed["parent_span_id"] = bits[3] if not parsed["parent_span_id"]: raise ValueError("Got empty ParentSpanId") if len(bits) > 2: # Empty-string means "missing" which means "Defer" if bits[2]: parsed["sampled_str"] = bits[2] if parsed["sampled_str"] not in ("0", "1", "d"): raise ValueError("Bad SampledState: %r" % parsed["sampled_str"]) return parsed def _parse_multi_header(headers: Dict[str, str]) -> B3JSON: """ Parse out and return the data necessary for generating ZipkinAttrs. Returns a dict with the following keys: 'trace_id': str or None 'span_id': str or None 'parent_span_id': str or None 'sampled_str': '0', '1', 'd', or None (defer) """ parsed: B3JSON = { "trace_id": headers.get("X-B3-TraceId", None), "span_id": headers.get("X-B3-SpanId", None), "parent_span_id": headers.get("X-B3-ParentSpanId", None), "sampled_str": headers.get("X-B3-Sampled", None), } # Normalize X-B3-Flags and X-B3-Sampled to None, '0', '1', or 'd' if headers.get("X-B3-Flags") == "1": parsed["sampled_str"] = "d" if parsed["sampled_str"] == "true": parsed["sampled_str"] = "1" elif parsed["sampled_str"] == "false": parsed["sampled_str"] = "0" if parsed["sampled_str"] not in (None, "1", "0", "d"): raise ValueError("Got invalid X-B3-Sampled: %s" % parsed["sampled_str"]) for k in ("trace_id", "span_id", "parent_span_id"): if parsed[k] == "": # type: ignore[literal-required] raise ValueError("Got empty-string %r" % k) if parsed["trace_id"] and not parsed["span_id"]: raise ValueError("Got X-B3-TraceId but not X-B3-SpanId") elif parsed["span_id"] and not parsed["trace_id"]: raise ValueError("Got X-B3-SpanId but not X-B3-TraceId") # Handle the common case of no headers at all if not parsed["trace_id"] and not parsed["sampled_str"]: raise ValueError() # won't trigger a log message return parsed def extract_zipkin_attrs_from_headers( headers: Dict[str, str], sample_rate: float = 100.0, use_128bit_trace_id: bool = False, ) -> Optional[ZipkinAttrs]: """ Implements extraction of B3 headers per: https://github.com/openzipkin/b3-propagation The input headers can be any dict-like container that supports "in" membership test and a .get() method that accepts a default value. Returns a ZipkinAttrs instance or None """ try: if "b3" in headers: parsed = _parse_single_header(headers["b3"]) else: parsed = _parse_multi_header(headers) except ValueError as e: if str(e): log.warning(e) return None # Handle the lone-sampling-decision case: if not parsed["trace_id"]: if parsed["sampled_str"] in ("1", "d"): sample_rate = 100.0 else: sample_rate = 0.0 attrs = create_attrs_for_span( sample_rate=sample_rate, use_128bit_trace_id=use_128bit_trace_id, flags="1" if parsed["sampled_str"] == "d" else "0", ) return attrs # Handle any sampling decision, including if it was deferred if parsed["sampled_str"]: # We have 1==Accept, 0==Deny, d==Debug if parsed["sampled_str"] in ("1", "d"): is_sampled = True else: is_sampled = False else: # sample flag missing; means "Defer" and we're responsible for # rolling fresh dice is_sampled = _should_sample(sample_rate) return ZipkinAttrs( parsed["trace_id"], parsed["span_id"], parsed["parent_span_id"], "1" if parsed["sampled_str"] == "d" else "0", is_sampled, ) def create_http_headers( context_stack: Optional[Stack] = None, tracer: Optional[Tracer] = None, new_span_id: bool = False, ) -> Dict[str, Optional[str]]: """ Generate the headers for a new zipkin span. .. note:: If the method is not called from within a zipkin_trace context, empty dict will be returned back. :returns: dict containing (X-B3-TraceId, X-B3-SpanId, X-B3-ParentSpanId, X-B3-Flags and X-B3-Sampled) keys OR an empty dict. """ if tracer: zipkin_attrs = tracer.get_zipkin_attrs() elif context_stack: zipkin_attrs = context_stack.get() else: zipkin_attrs = get_default_tracer().get_zipkin_attrs() # If zipkin_attrs is still not set then we're not in a trace context if not zipkin_attrs: return {} if new_span_id: span_id: Optional[str] = generate_random_64bit_string() parent_span_id = zipkin_attrs.span_id else: span_id = zipkin_attrs.span_id parent_span_id = zipkin_attrs.parent_span_id return { "X-B3-TraceId": zipkin_attrs.trace_id, "X-B3-SpanId": span_id, "X-B3-ParentSpanId": parent_span_id, "X-B3-Flags": "0", "X-B3-Sampled": "1" if zipkin_attrs.is_sampled else "0", } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663591.0 py_zipkin-1.2.8/py_zipkin/storage.py0000644000175100001730000001740100000000000020375 0ustar00runnerdocker00000000000000import logging import threading from typing import Any from typing import Deque from typing import List from typing import Optional from typing import TYPE_CHECKING from py_zipkin.encoding._helpers import Span from py_zipkin.util import ZipkinAttrs if TYPE_CHECKING: # pragma: no cover from py_zipkin import zipkin try: # pragma: no cover # Since python 3.7 threadlocal is deprecated in favor of contextvars # which also work in asyncio. import contextvars _contextvars_tracer: Optional[ contextvars.ContextVar["Tracer"] ] = contextvars.ContextVar("py_zipkin.Tracer object") except ImportError: # pragma: no cover # The contextvars module was added in python 3.7 _contextvars_tracer = None _thread_local_tracer = threading.local() log = logging.getLogger("py_zipkin.storage") def _get_thread_local_tracer() -> "Tracer": """Returns the current tracer from thread-local. If there's no current tracer it'll create a new one. :returns: current tracer. :rtype: Tracer """ if not hasattr(_thread_local_tracer, "tracer"): _thread_local_tracer.tracer = Tracer() return _thread_local_tracer.tracer def _set_thread_local_tracer(tracer: "Tracer") -> None: """Sets the current tracer in thread-local. :param tracer: current tracer. :type tracer: Tracer """ _thread_local_tracer.tracer = tracer def _get_contextvars_tracer() -> "Tracer": # pragma: no cover """Returns the current tracer from contextvars. If there's no current tracer it'll create a new one. :returns: current tracer. :rtype: Tracer """ assert _contextvars_tracer is not None try: return _contextvars_tracer.get() except LookupError: _contextvars_tracer.set(Tracer()) return _contextvars_tracer.get() def _set_contextvars_tracer(tracer: "Tracer") -> None: # pragma: no cover """Sets the current tracer in contextvars. :param tracer: current tracer. :type tracer: Tracer """ assert _contextvars_tracer is not None _contextvars_tracer.set(tracer) class Tracer: def __init__(self) -> None: self._is_transport_configured = False self._span_storage = SpanStorage() self._context_stack = Stack() def get_zipkin_attrs(self) -> Optional[ZipkinAttrs]: return self._context_stack.get() def push_zipkin_attrs(self, ctx: ZipkinAttrs) -> None: self._context_stack.push(ctx) def pop_zipkin_attrs(self) -> Optional[ZipkinAttrs]: return self._context_stack.pop() def add_span(self, span: Span) -> None: self._span_storage.append(span) def get_spans(self) -> "SpanStorage": return self._span_storage def clear(self) -> None: self._span_storage.clear() def set_transport_configured(self, configured: bool) -> None: self._is_transport_configured = configured def is_transport_configured(self) -> bool: return self._is_transport_configured def zipkin_span(self, *argv: Any, **kwargs: Any) -> "zipkin.zipkin_span": from py_zipkin import zipkin kwargs["_tracer"] = self return zipkin.zipkin_span(*argv, **kwargs) def copy(self) -> "Tracer": """Return a copy of this instance, but with a deep-copied _context_stack. The use-case is for passing a copy of a Tracer into a new thread context. """ the_copy = self.__class__() the_copy._is_transport_configured = self._is_transport_configured the_copy._span_storage = self._span_storage the_copy._context_stack = self._context_stack.copy() return the_copy class Stack: """ Stack is a simple stack class. It offers the operations push, pop and get. The latter two return None if the stack is empty. .. deprecated:: Use the Tracer interface which offers better multi-threading support. Stack will be removed in version 1.0. """ def __init__(self, storage: Optional[List[ZipkinAttrs]] = None) -> None: if storage is not None: log.warning("Passing a storage object to Stack is deprecated.") self.__storage: List[ZipkinAttrs] = storage else: self.__storage = [] # this pattern is currently necessary due to # https://github.com/python/mypy/issues/4125 @property def _storage(self) -> List[ZipkinAttrs]: return self.__storage @_storage.setter def _storage(self, value: List[ZipkinAttrs]) -> None: # pragma: no cover self.__storage = value @_storage.deleter def _storage(self) -> None: # pragma: no cover del self.__storage def push(self, item: ZipkinAttrs) -> None: self._storage.append(item) def pop(self) -> Optional[ZipkinAttrs]: if self._storage: return self._storage.pop() return None def get(self) -> Optional[ZipkinAttrs]: if self._storage: return self._storage[-1] return None def copy(self) -> "Stack": # Return a new Stack() instance with a deep copy of our stack contents the_copy = self.__class__() the_copy._storage = self._storage[:] return the_copy class ThreadLocalStack(Stack): """ThreadLocalStack is variant of Stack that uses a thread local storage. The thread local storage is accessed lazily in every method call, so the thread that calls the method matters, not the thread that instantiated the class. Every instance shares the same thread local data. .. deprecated:: Use the Tracer interface which offers better multi-threading support. ThreadLocalStack will be removed in version 1.0. """ def __init__(self) -> None: log.warning( "ThreadLocalStack is deprecated. See DEPRECATIONS.rst for" "details on how to migrate to using Tracer." ) @property def _storage(self) -> List[ZipkinAttrs]: return get_default_tracer()._context_stack._storage @_storage.setter def _storage(self, value: List[ZipkinAttrs]) -> None: # pragma: no cover get_default_tracer()._context_stack._storage = value @_storage.deleter def _storage(self) -> None: # pragma: no cover del get_default_tracer()._context_stack._storage class SpanStorage(Deque[Span]): """Stores the list of completed spans ready to be sent. .. deprecated:: Use the Tracer interface which offers better multi-threading support. SpanStorage will be removed in version 1.0. """ pass def default_span_storage() -> SpanStorage: log.warning( "default_span_storage is deprecated. See DEPRECATIONS.rst for" "details on how to migrate to using Tracer." ) return get_default_tracer()._span_storage def has_default_tracer() -> bool: """Is there a default tracer created already? :returns: Is there a default tracer created already? :rtype: boolean """ try: if _contextvars_tracer and _contextvars_tracer.get(): return True except LookupError: pass return hasattr(_thread_local_tracer, "tracer") def get_default_tracer() -> Tracer: """Return the current default Tracer. For now it'll get it from thread-local in Python 2.7 to 3.6 and from contextvars since Python 3.7. :returns: current default tracer. :rtype: Tracer """ if _contextvars_tracer: return _get_contextvars_tracer() return _get_thread_local_tracer() def set_default_tracer(tracer: Tracer) -> None: """Sets the current default Tracer. For now it'll get it from thread-local in Python 2.7 to 3.6 and from contextvars since Python 3.7. :returns: current default tracer. :rtype: Tracer """ if _contextvars_tracer: _set_contextvars_tracer(tracer) _set_thread_local_tracer(tracer) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1679663596.5611188 py_zipkin-1.2.8/py_zipkin/testing/0000755000175100001730000000000000000000000020031 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663591.0 py_zipkin-1.2.8/py_zipkin/testing/__init__.py0000644000175100001730000000012000000000000022133 0ustar00runnerdocker00000000000000from py_zipkin.testing.mock_transport import MockTransportHandler # noqa: F401 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663591.0 py_zipkin-1.2.8/py_zipkin/testing/mock_transport.py0000644000175100001730000000416000000000000023451 0ustar00runnerdocker00000000000000from typing import List from typing import Optional from typing import Union from py_zipkin.transport import BaseTransportHandler class MockTransportHandler(BaseTransportHandler): """Mock transport for use in tests. It doesn't emit anything and just stores the generated spans in memory. To check what has been emitted you can use `get_payloads` and get back the list of encoded spans that were emitted. To use it: .. code-block:: python transport = MockTransportHandler() with zipkin.zipkin_span( service_name='test_service_name', span_name='test_span_name', transport_handler=transport, sample_rate=100.0, encoding=Encoding.V2_JSON, ): do_something() spans = transport.get_payloads() assert len(spans) == 1 decoded_spans = json.loads(spans[0]) assert decoded_spans == [{}] """ def __init__(self, max_payload_bytes: Optional[int] = None) -> None: """Creates a new MockTransportHandler. :param max_payload_bytes: max payload size in bytes. You often don't need to set this in tests unless you want to test what happens when your spans are bigger than the maximum payload size. :type max_payload_bytes: int """ self.max_payload_bytes = max_payload_bytes self.payloads: List[Union[bytes, str]] = [] def send(self, payload: Union[bytes, str]) -> None: """Overrides the real send method. Should not be called directly.""" self.payloads.append(payload) return payload # type: ignore[return-value] def get_max_payload_bytes(self) -> Optional[int]: """Overrides the real method. Should not be called directly.""" return self.max_payload_bytes def get_payloads(self) -> List[Union[bytes, str]]: """Returns the encoded spans that were sent. Spans are batched before being sent, so most of the time the returned list will contain only one element. Each element is gonna be an encoded list of spans. """ return self.payloads ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663591.0 py_zipkin-1.2.8/py_zipkin/thread_local.py0000644000175100001730000000670400000000000021356 0ustar00runnerdocker00000000000000import logging from typing import Deque from typing import List from typing import Optional from py_zipkin.encoding._helpers import Span from py_zipkin.storage import get_default_tracer from py_zipkin.util import ZipkinAttrs log = logging.getLogger("py_zipkin.thread_local") def get_thread_local_zipkin_attrs() -> List[ZipkinAttrs]: """A wrapper to return _thread_local.zipkin_attrs Returns a list of ZipkinAttrs objects, used for intra-process context propagation. .. deprecated:: Use the Tracer interface which offers better multi-threading support. get_thread_local_zipkin_attrs will be removed in version 1.0. :returns: list that may contain zipkin attribute tuples :rtype: list """ log.warning( "get_thread_local_zipkin_attrs is deprecated. See DEPRECATIONS.rst" " for details on how to migrate to using Tracer." ) return get_default_tracer()._context_stack._storage def get_thread_local_span_storage() -> Deque[Span]: """A wrapper to return _thread_local.span_storage Returns a SpanStorage object used to temporarily store all spans created in the current process. The transport handlers will pull from this storage when they emit the spans. .. deprecated:: Use the Tracer interface which offers better multi-threading support. get_thread_local_span_storage will be removed in version 1.0. :returns: SpanStore object containing all non-root spans. :rtype: py_zipkin.storage.SpanStore """ log.warning( "get_thread_local_span_storage is deprecated. See DEPRECATIONS.rst" " for details on how to migrate to using Tracer." ) return get_default_tracer()._span_storage def get_zipkin_attrs() -> Optional[ZipkinAttrs]: """Get the topmost level zipkin attributes stored. .. deprecated:: Use the Tracer interface which offers better multi-threading support. get_zipkin_attrs will be removed in version 1.0. :returns: tuple containing zipkin attrs :rtype: :class:`zipkin.ZipkinAttrs` """ from py_zipkin.storage import ThreadLocalStack log.warning( "get_zipkin_attrs is deprecated. See DEPRECATIONS.rst for" "details on how to migrate to using Tracer." ) return ThreadLocalStack().get() def pop_zipkin_attrs() -> Optional[ZipkinAttrs]: """Pop the topmost level zipkin attributes, if present. .. deprecated:: Use the Tracer interface which offers better multi-threading support. pop_zipkin_attrs will be removed in version 1.0. :returns: tuple containing zipkin attrs :rtype: :class:`zipkin.ZipkinAttrs` """ from py_zipkin.storage import ThreadLocalStack log.warning( "pop_zipkin_attrs is deprecated. See DEPRECATIONS.rst for" "details on how to migrate to using Tracer." ) return ThreadLocalStack().pop() def push_zipkin_attrs(zipkin_attr: ZipkinAttrs) -> None: """Stores the zipkin attributes to thread local. .. deprecated:: Use the Tracer interface which offers better multi-threading support. push_zipkin_attrs will be removed in version 1.0. :param zipkin_attr: tuple containing zipkin related attrs :type zipkin_attr: :class:`zipkin.ZipkinAttrs` """ from py_zipkin.storage import ThreadLocalStack log.warning( "push_zipkin_attrs is deprecated. See DEPRECATIONS.rst for" "details on how to migrate to using Tracer." ) ThreadLocalStack().push(zipkin_attr) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663591.0 py_zipkin-1.2.8/py_zipkin/transport.py0000644000175100001730000001035500000000000020766 0ustar00runnerdocker00000000000000from typing import Optional from typing import Tuple from typing import Union from urllib.request import Request from urllib.request import urlopen from py_zipkin.encoding import detect_span_version_and_encoding from py_zipkin.encoding import Encoding class BaseTransportHandler: def get_max_payload_bytes(self) -> Optional[int]: # pragma: no cover """Returns the maximum payload size for this transport. Most transports have a maximum packet size that can be sent. For example, UDP has a 65507 bytes MTU. py_zipkin automatically batches collected spans for performance reasons. The batch size is going to be the minimum between `get_max_payload_bytes` and `max_span_batch_size` from `zipkin_span`. If you don't want to enforce a max payload size, return None. :returns: max payload size in bytes or None. """ raise NotImplementedError("get_max_payload_bytes is not implemented") def send(self, payload: Union[bytes, str]) -> None: # pragma: no cover """Sends the encoded payload over the transport. :argument payload: encoded list of spans. """ raise NotImplementedError("send is not implemented") def __call__(self, payload: Union[bytes, str]) -> None: """Internal wrapper around `send`. Do not override. Mostly used to keep backward compatibility with older transports implemented as functions. However decoupling the function developers override and what's internally called by py_zipkin will allow us to add extra logic here in the future without having the users update their code every time. """ self.send(payload) class UnknownEncoding(Exception): """Exception class for when encountering an unknown Encoding""" class SimpleHTTPTransport(BaseTransportHandler): def __init__(self, address: str, port: int) -> None: """A simple HTTP transport for zipkin. This is not production ready (not async, no retries) but it's helpful for tests or people trying out py-zipkin. .. code-block:: python with zipkin_span( service_name='my_service', span_name='home', sample_rate=100, transport_handler=SimpleHTTPTransport('localhost', 9411), encoding=Encoding.V2_JSON, ): pass :param address: zipkin server address. :type address: str :param port: zipkin server port. :type port: int """ super().__init__() self.address = address self.port = port def get_max_payload_bytes(self) -> Optional[int]: return None def _get_path_content_type(self, payload: Union[str, bytes]) -> Tuple[str, str]: """Choose the right api path and content type depending on the encoding. This is not something you'd need to do generally when writing your own transport since in that case you'd know which encoding you're using. Since this is a generic transport, we need to make it compatible with any encoding instead. """ encoded_payload = ( payload.encode("utf-8") if isinstance(payload, str) else payload ) encoding = detect_span_version_and_encoding(encoded_payload) if encoding == Encoding.V1_JSON: return "/api/v1/spans", "application/json" elif encoding == Encoding.V1_THRIFT: return "/api/v1/spans", "application/x-thrift" elif encoding == Encoding.V2_JSON: return "/api/v2/spans", "application/json" elif encoding == Encoding.V2_PROTO3: return "/api/v2/spans", "application/x-protobuf" else: # pragma: nocover raise UnknownEncoding(f"Unknown encoding: {encoding}") def send(self, payload: Union[str, bytes]) -> None: encoded_payload = ( payload.encode("utf-8") if isinstance(payload, str) else payload ) path, content_type = self._get_path_content_type(encoded_payload) url = f"http://{self.address}:{self.port}{path}" req = Request(url, encoded_payload, {"Content-Type": content_type}) response = urlopen(req) assert response.getcode() == 202 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663591.0 py_zipkin-1.2.8/py_zipkin/util.py0000644000175100001730000000775600000000000017722 0ustar00runnerdocker00000000000000import random import struct import time from typing import NamedTuple from typing import Optional class ZipkinAttrs(NamedTuple): """ Holds the basic attributes needed to log a zipkin trace :param trace_id: Unique trace id :param span_id: Span Id of the current request span :param parent_span_id: Parent span Id of the current request span :param flags: stores flags header. Currently unused :param is_sampled: pre-computed bool whether the trace should be logged """ trace_id: str span_id: Optional[str] parent_span_id: Optional[str] flags: str is_sampled: bool def generate_random_64bit_string() -> str: """Returns a 64 bit UTF-8 encoded string. In the interests of simplicity, this is always cast to a `str` instead of (in py2 land) a unicode string. Certain clients (I'm looking at you, Twisted) don't enjoy unicode headers. :returns: random 16-character string """ return f"{random.getrandbits(64):016x}" def generate_random_128bit_string() -> str: """Returns a 128 bit UTF-8 encoded string. Follows the same conventions as generate_random_64bit_string(). The upper 32 bits are the current time in epoch seconds, and the lower 96 bits are random. This allows for AWS X-Ray `interop `_ :returns: 32-character hex string """ t = int(time.time()) lower_96 = random.getrandbits(96) return f"{(t << 96) | lower_96:032x}" def unsigned_hex_to_signed_int(hex_string: str) -> int: """Converts a 64-bit hex string to a signed int value. This is due to the fact that Apache Thrift only has signed values. Examples: '17133d482ba4f605' => 1662740067609015813 'b6dbb1c2b362bf51' => -5270423489115668655 :param hex_string: the string representation of a zipkin ID :returns: signed int representation """ return struct.unpack("q", struct.pack("Q", int(hex_string, 16)))[0] def signed_int_to_unsigned_hex(signed_int: int) -> str: """Converts a signed int value to a 64-bit hex string. Examples: 1662740067609015813 => '17133d482ba4f605' -5270423489115668655 => 'b6dbb1c2b362bf51' :param signed_int: an int to convert :returns: unsigned hex string """ hex_string = hex(struct.unpack("Q", struct.pack("q", signed_int))[0])[2:] if hex_string.endswith("L"): return hex_string[:-1] return hex_string def _should_sample(sample_rate: float) -> bool: if sample_rate == 0.0: return False # save a die roll elif sample_rate == 100.0: return True # ditto return (random.random() * 100) < sample_rate def create_attrs_for_span( sample_rate: float = 100.0, trace_id: Optional[str] = None, span_id: Optional[str] = None, use_128bit_trace_id: bool = False, flags: Optional[str] = None, ) -> ZipkinAttrs: """Creates a set of zipkin attributes for a span. :param sample_rate: Float between 0.0 and 100.0 to determine sampling rate :type sample_rate: float :param trace_id: Optional 16-character hex string representing a trace_id. If this is None, a random trace_id will be generated. :type trace_id: str :param span_id: Optional 16-character hex string representing a span_id. If this is None, a random span_id will be generated. :type span_id: str :param use_128bit_trace_id: If true, generate 128-bit trace_ids :type use_128bit_trace_id: bool """ # Calculate if this trace is sampled based on the sample rate if trace_id is None: if use_128bit_trace_id: trace_id = generate_random_128bit_string() else: trace_id = generate_random_64bit_string() if span_id is None: span_id = generate_random_64bit_string() is_sampled = _should_sample(sample_rate) return ZipkinAttrs( trace_id=trace_id, span_id=span_id, parent_span_id=None, flags=flags or "0", is_sampled=is_sampled, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663591.0 py_zipkin-1.2.8/py_zipkin/zipkin.py0000644000175100001730000007262700000000000020250 0ustar00runnerdocker00000000000000import functools import logging import time from types import TracebackType from typing import Any from typing import Callable from typing import cast from typing import Dict from typing import Optional from typing import Tuple from typing import Type from typing import TypeVar from py_zipkin import Encoding from py_zipkin import Kind from py_zipkin import storage from py_zipkin.encoding._helpers import create_endpoint from py_zipkin.encoding._helpers import Endpoint from py_zipkin.encoding._helpers import Span from py_zipkin.exception import ZipkinError from py_zipkin.logging_helper import TransportHandler from py_zipkin.logging_helper import ZipkinLoggingContext from py_zipkin.request_helpers import create_http_headers from py_zipkin.storage import get_default_tracer from py_zipkin.storage import SpanStorage from py_zipkin.storage import Stack from py_zipkin.storage import Tracer from py_zipkin.util import create_attrs_for_span from py_zipkin.util import generate_random_64bit_string from py_zipkin.util import ZipkinAttrs log = logging.getLogger(__name__) ERROR_KEY = "error" F = TypeVar("F", bound=Callable[..., Any]) class zipkin_span: """Context manager/decorator for all of your zipkin tracing needs. Usage #1: Start a trace with a given sampling rate This begins the zipkin trace and also records the root span. The required params are service_name, transport_handler, and sample_rate. # Start a trace with do_stuff() as the root span def some_batch_job(a, b): with zipkin_span( service_name='my_service', span_name='my_span_name', transport_handler=some_handler, port=22, sample_rate=0.05, ): do_stuff() Usage #2: Trace a service call. The typical use case is instrumenting a framework like Pyramid or Django. Only ss and sr times are recorded for the root span. Required params are service_name, zipkin_attrs, transport_handler, and port. # Used in a pyramid tween def tween(request): zipkin_attrs = some_zipkin_attr_creator(request) with zipkin_span( service_name='my_service,' span_name='my_span_name', zipkin_attrs=zipkin_attrs, transport_handler=some_handler, port=22, ) as zipkin_context: response = handler(request) zipkin_context.update_binary_annotations( some_binary_annotations) return response Usage #3: Log a span within the context of a zipkin trace If you're already in a zipkin trace, you can use this to log a span inside. The only required param is service_name. If you're not in a zipkin trace, this won't do anything. # As a decorator @zipkin_span(service_name='my_service', span_name='my_function') def my_function(): do_stuff() # As a context manager def my_function(): with zipkin_span(service_name='my_service', span_name='do_stuff'): do_stuff() """ def __init__( self, service_name: str, span_name: str = "span", zipkin_attrs: Optional[ZipkinAttrs] = None, transport_handler: Optional[TransportHandler] = None, max_span_batch_size: Optional[int] = None, annotations: Optional[Dict[str, Optional[float]]] = None, binary_annotations: Optional[Dict[str, Optional[str]]] = None, port: int = 0, sample_rate: Optional[float] = None, include: Optional[str] = None, add_logging_annotation: bool = False, report_root_timestamp: bool = False, use_128bit_trace_id: bool = False, host: Optional[str] = None, context_stack: Optional[Stack] = None, span_storage: Optional[SpanStorage] = None, firehose_handler: Optional[TransportHandler] = None, kind: Optional[Kind] = None, timestamp: Optional[float] = None, duration: Optional[float] = None, encoding: Encoding = Encoding.V2_JSON, _tracer: Optional[Tracer] = None, ): """Logs a zipkin span. If this is the root span, then a zipkin trace is started as well. :param service_name: The name of the called service :type service_name: string :param span_name: Optional name of span, defaults to 'span' :type span_name: string :param zipkin_attrs: Optional set of zipkin attributes to be used :type zipkin_attrs: ZipkinAttrs :param transport_handler: Callback function that takes a message parameter and handles logging it :type transport_handler: BaseTransportHandler :param max_span_batch_size: Spans in a trace are sent in batches, max_span_batch_size defines max size of one batch :type max_span_batch_size: int :param annotations: Optional dict of str -> timestamp annotations :type annotations: dict of str -> int :param binary_annotations: Optional dict of str -> str span attrs :type binary_annotations: dict of str -> str :param port: The port number of the service. Defaults to 0. :type port: int :param sample_rate: Rate at which to sample; 0.0 - 100.0. If passed-in zipkin_attrs have is_sampled=False and the sample_rate param is > 0, a new span will be generated at this rate. This means that if you propagate sampling decisions to downstream services, but still have sample_rate > 0 in those services, the actual rate of generated spans for those services will be > sampling_rate. :type sample_rate: float :param include: which annotations to include can be one of {'client', 'server'} corresponding to ('cs', 'cr') and ('ss', 'sr') respectively. DEPRECATED: use kind instead. `include` will be removed in 1.0. :type include: iterable :param add_logging_annotation: Whether to add a 'logging_end' annotation when py_zipkin finishes logging spans :type add_logging_annotation: boolean :param report_root_timestamp: Whether the span should report timestamp and duration. Only applies to "root" spans in this local context, so spans created inside other span contexts will always log timestamp/duration. Note that this is only an override for spans that have zipkin_attrs passed in. Spans that make their own sampling decisions (i.e. are the root spans of entire traces) will always report timestamp/duration. :type report_root_timestamp: boolean :param use_128bit_trace_id: If true, generate 128-bit trace_ids. :type use_128bit_trace_id: boolean :param host: Contains the ipv4 or ipv6 value of the host. The ip value isn't automatically determined in a docker environment. :type host: string :param context_stack: explicit context stack for storing zipkin attributes :type context_stack: object :param span_storage: explicit Span storage for storing zipkin spans before they're emitted. :type span_storage: py_zipkin.storage.SpanStorage :param firehose_handler: [EXPERIMENTAL] Similar to transport_handler, except that it will receive 100% of the spans regardless of trace sampling rate. :type firehose_handler: BaseTransportHandler :param kind: Span type (client, server, local, etc...). :type kind: Kind :param timestamp: Timestamp in seconds, defaults to `time.time()`. Set this if you want to use a custom timestamp. :type timestamp: float :param duration: Duration in seconds, defaults to the time spent in the context. Set this if you want to use a custom duration. :type duration: float :param encoding: Output encoding format, defaults to V2_JSON spans. :type encoding: Encoding :param _tracer: Current tracer object. This argument is passed in automatically when you create a zipkin_span from a Tracer. :type _tracer: Tracer """ self.service_name = service_name self.span_name = span_name self.zipkin_attrs_override = zipkin_attrs self.transport_handler = transport_handler self.max_span_batch_size = max_span_batch_size self.annotations = annotations or {} self.binary_annotations = binary_annotations or {} self.port = port self.sample_rate = sample_rate self.add_logging_annotation = add_logging_annotation self.report_root_timestamp_override = report_root_timestamp self.use_128bit_trace_id = use_128bit_trace_id self.host = host self._context_stack = context_stack self._span_storage = span_storage self.firehose_handler = firehose_handler self.kind = self._generate_kind(kind, include) self.timestamp = timestamp self.duration = duration self.encoding = encoding self._tracer = _tracer self._is_local_root_span = False self.logging_context: Optional[ZipkinLoggingContext] = None self.do_pop_attrs = False # Spans that log a 'cs' timestamp can additionally record a # 'sa' binary annotation that shows where the request is going. self.remote_endpoint: Optional[Endpoint] = None self.zipkin_attrs: Optional[ZipkinAttrs] = None # It used to be possible to override timestamp and duration by passing # in the cs/cr or sr/ss annotations. We want to keep backward compatibility # for now, so this logic overrides self.timestamp and self.duration in the # same way. # This doesn't fit well with v2 spans since those annotations are gone, so # we also log a deprecation warning. if "sr" in self.annotations and "ss" in self.annotations: assert self.annotations["ss"] is not None assert self.annotations["sr"] is not None self.duration = self.annotations["ss"] - self.annotations["sr"] self.timestamp = self.annotations["sr"] log.warning( "Manually setting 'sr'/'ss' annotations is deprecated. Please " "use the timestamp and duration parameters." ) if "cr" in self.annotations and "cs" in self.annotations: assert self.annotations["cr"] is not None assert self.annotations["cs"] is not None self.duration = self.annotations["cr"] - self.annotations["cs"] self.timestamp = self.annotations["cs"] log.warning( "Manually setting 'cr'/'cs' annotations is deprecated. Please " "use the timestamp and duration parameters." ) # Root spans have transport_handler and at least one of # zipkin_attrs_override or sample_rate. if self.zipkin_attrs_override or self.sample_rate is not None: # transport_handler is mandatory for root spans if self.transport_handler is None: raise ZipkinError("Root spans require a transport handler to be given") self._is_local_root_span = True # If firehose_handler than this is a local root span. if self.firehose_handler: self._is_local_root_span = True if self.sample_rate is not None and not (0.0 <= self.sample_rate <= 100.0): raise ZipkinError("Sample rate must be between 0.0 and 100.0") if self._span_storage is not None and not isinstance( self._span_storage, storage.SpanStorage ): raise ZipkinError( "span_storage should be an instance of py_zipkin.storage.SpanStorage" ) if self._span_storage is not None: log.warning("span_storage is deprecated. Set local_storage instead.") self.get_tracer()._span_storage = self._span_storage if self._context_stack is not None: log.warning("context_stack is deprecated. Set local_storage instead.") self.get_tracer()._context_stack = self._context_stack def __call__(self, f: F) -> F: @functools.wraps(f) def decorated(*args: Any, **kwargs: Any) -> Any: with zipkin_span( service_name=self.service_name, span_name=self.span_name, zipkin_attrs=self.zipkin_attrs, transport_handler=self.transport_handler, max_span_batch_size=self.max_span_batch_size, annotations=self.annotations, binary_annotations=self.binary_annotations, port=self.port, sample_rate=self.sample_rate, include=None, add_logging_annotation=self.add_logging_annotation, report_root_timestamp=self.report_root_timestamp_override, use_128bit_trace_id=self.use_128bit_trace_id, host=self.host, context_stack=self._context_stack, span_storage=self._span_storage, firehose_handler=self.firehose_handler, kind=self.kind, timestamp=self.timestamp, duration=self.duration, encoding=self.encoding, _tracer=self._tracer, ): return f(*args, **kwargs) return cast(F, decorated) def get_tracer(self) -> Tracer: if self._tracer is not None: return self._tracer else: return get_default_tracer() def __enter__(self) -> "zipkin_span": return self.start() def _generate_kind(self, kind: Optional[Kind], include: Optional[str]) -> Kind: # If `kind` is not set, then we generate it from `include`. # This code maintains backward compatibility with old versions of py_zipkin # which used include rather than kind to identify client / server spans. if kind: return kind else: if include: # If `include` contains only one of `client` or `server` # than it's a client or server span respectively. # If neither or both are present, then it's a local span # which is represented by kind = None. log.warning("The include argument is deprecated. Please use kind.") if "client" in include and "server" not in include: return Kind.CLIENT elif "client" not in include and "server" in include: return Kind.SERVER else: return Kind.LOCAL # If both kind and include are unset, then it's a local span. return Kind.LOCAL def _get_current_context(self) -> Tuple[bool, Optional[ZipkinAttrs]]: """Returns the current ZipkinAttrs and generates new ones if needed. :returns: (report_root_timestamp, zipkin_attrs) :rtype: (bool, ZipkinAttrs) """ # This check is technically not necessary since only root spans will have # sample_rate, zipkin_attrs or a transport set. But it helps making the # code clearer by separating the logic for a root span from the one for a # child span. if self._is_local_root_span: # If sample_rate is set, we need to (re)generate a trace context. # If zipkin_attrs (trace context) were passed in as argument there are # 2 possibilities: # is_sampled = False --> we keep the same trace_id but re-roll the dice # for is_sampled. # is_sampled = True --> we don't want to stop sampling halfway through # a sampled trace, so we do nothing. # If no zipkin_attrs were passed in, we generate new ones and start a # new trace. if self.sample_rate is not None: # If this trace is not sampled, we re-roll the dice. if ( self.zipkin_attrs_override and not self.zipkin_attrs_override.is_sampled ): # This will be the root span of the trace, so we should # set timestamp and duration. return ( True, create_attrs_for_span( sample_rate=self.sample_rate, trace_id=self.zipkin_attrs_override.trace_id, ), ) # If zipkin_attrs_override was not passed in, we simply generate # new zipkin_attrs to start a new trace. elif not self.zipkin_attrs_override: return ( True, create_attrs_for_span( sample_rate=self.sample_rate, use_128bit_trace_id=self.use_128bit_trace_id, ), ) if self.firehose_handler and not self.zipkin_attrs_override: # If it has gotten here, the only thing that is # causing a trace is the firehose. So we force a trace # with sample rate of 0 return ( True, create_attrs_for_span( sample_rate=0.0, use_128bit_trace_id=self.use_128bit_trace_id, ), ) # If we arrive here it means the sample_rate was not set while # zipkin_attrs_override was, so let's simply return that. return False, self.zipkin_attrs_override else: # Check if there's already a trace context in _context_stack. existing_zipkin_attrs = self.get_tracer().get_zipkin_attrs() # If there's an existing context, let's create new zipkin_attrs # with that context as parent. if existing_zipkin_attrs: return ( False, ZipkinAttrs( trace_id=existing_zipkin_attrs.trace_id, span_id=generate_random_64bit_string(), parent_span_id=existing_zipkin_attrs.span_id, flags=existing_zipkin_attrs.flags, is_sampled=existing_zipkin_attrs.is_sampled, ), ) return False, None def start(self) -> "zipkin_span": """Enter the new span context. All annotations logged inside this context will be attributed to this span. All new spans generated inside this context will have this span as their parent. In the unsampled case, this context still generates new span IDs and pushes them onto the threadlocal stack, so downstream services calls made will pass the correct headers. However, the logging handler is never attached in the unsampled case, so the spans are never logged. """ self.do_pop_attrs = False report_root_timestamp, self.zipkin_attrs = self._get_current_context() # If zipkin_attrs are not set up by now, that means this span is not # configured to perform logging itself, and it's not in an existing # Zipkin trace. That means there's nothing else to do and it can exit # early. if not self.zipkin_attrs: return self self.get_tracer().push_zipkin_attrs(self.zipkin_attrs) self.do_pop_attrs = True self.start_timestamp = time.time() if self._is_local_root_span: # Don't set up any logging if we're not sampling if not self.zipkin_attrs.is_sampled and not self.firehose_handler: return self # If transport is already configured don't override it. Doing so would # cause all previously recorded spans to never be emitted as exiting # the inner logging context will reset transport_configured to False. if self.get_tracer().is_transport_configured(): log.info( "Transport was already configured, ignoring override " "from span {}".format(self.span_name) ) return self endpoint = create_endpoint(self.port, self.service_name, self.host) self.logging_context = ZipkinLoggingContext( self.zipkin_attrs, endpoint, self.span_name, self.transport_handler, report_root_timestamp or self.report_root_timestamp_override, self.get_tracer, self.service_name, binary_annotations=self.binary_annotations, add_logging_annotation=self.add_logging_annotation, client_context=self.kind == Kind.CLIENT, max_span_batch_size=self.max_span_batch_size, firehose_handler=self.firehose_handler, encoding=self.encoding, annotations=self.annotations, ) self.logging_context.start() self.get_tracer().set_transport_configured(configured=True) return self def __exit__( self, _exc_type: Optional[Type[BaseException]], _exc_value: Optional[BaseException], _exc_traceback: TracebackType, ) -> None: self.stop(_exc_type, _exc_value, _exc_traceback) def stop( self, _exc_type: Optional[Type[BaseException]] = None, _exc_value: Optional[BaseException] = None, _exc_traceback: Optional[TracebackType] = None, ) -> None: """Exit the span context. Zipkin attrs are pushed onto the threadlocal stack regardless of sampling, so they always need to be popped off. The actual logging of spans depends on sampling and that the logging was correctly set up. """ if self.do_pop_attrs: self.get_tracer().pop_zipkin_attrs() # If no transport is configured, there's no reason to create a new Span. # This also helps avoiding memory leaks since without a transport nothing # would pull spans out of get_tracer(). if not self.get_tracer().is_transport_configured(): return # Add the error annotation if an exception occurred if any((_exc_type, _exc_value, _exc_traceback)): assert _exc_type is not None try: error_msg = f"{_exc_type.__name__}: {_exc_value}" except TypeError: # This sometimes happens when an exception raises when calling # __str__ on it. error_msg = f"{_exc_type.__name__}: {_exc_value!r}" self.update_binary_annotations({ERROR_KEY: error_msg}) # Logging context is only initialized for "root" spans of the local # process (i.e. this zipkin_span not inside of any other local # zipkin_spans) if self.logging_context: try: self.logging_context.stop() except Exception as ex: err_msg = f"Error emitting zipkin trace. {repr(ex)}" log.error(err_msg) finally: self.logging_context = None self.get_tracer().clear() self.get_tracer().set_transport_configured(configured=False) return # If we've gotten here, that means that this span is a child span of # this context's root span (i.e. it's a zipkin_span inside another # zipkin_span). end_timestamp = time.time() # If self.duration is set, it means the user wants to override it if self.duration: duration = self.duration else: duration = end_timestamp - self.start_timestamp endpoint = create_endpoint(self.port, self.service_name, self.host) assert self.zipkin_attrs is not None self.get_tracer().add_span( Span( trace_id=self.zipkin_attrs.trace_id, name=self.span_name, parent_id=self.zipkin_attrs.parent_span_id, span_id=self.zipkin_attrs.span_id, kind=self.kind, timestamp=self.timestamp if self.timestamp else self.start_timestamp, duration=duration, annotations=self.annotations, local_endpoint=endpoint, remote_endpoint=self.remote_endpoint, tags=self.binary_annotations, ) ) def update_binary_annotations( self, extra_annotations: Dict[str, Optional[str]] ) -> None: """Updates the binary annotations for the current span.""" if not self.logging_context: # This is not the root span, so binary annotations will be added # to the log handler when this span context exits. self.binary_annotations.update(extra_annotations) else: # Otherwise, we're in the context of the root span, so just update # the binary annotations for the logging context directly. self.logging_context.tags.update(extra_annotations) def add_annotation(self, value: str, timestamp: Optional[float] = None) -> None: """Add an annotation for the current span The timestamp defaults to "now", but may be specified. :param value: The annotation string :type value: str :param timestamp: Timestamp for the annotation :type timestamp: float """ timestamp = timestamp or time.time() if not self.logging_context: # This is not the root span, so annotations will be added # to the log handler when this span context exits. self.annotations[value] = timestamp else: # Otherwise, we're in the context of the root span, so just update # the annotations for the logging context directly. self.logging_context.annotations[value] = timestamp def add_sa_binary_annotation( self, port: int = 0, service_name: str = "unknown", host: str = "127.0.0.1", ) -> None: """Adds a 'sa' binary annotation to the current span. 'sa' binary annotations are useful for situations where you need to log where a request is going but the destination doesn't support zipkin. Note that the span must have 'cs'/'cr' annotations. :param port: The port number of the destination :type port: int :param service_name: The name of the destination service :type service_name: str :param host: Host address of the destination :type host: str """ if self.kind != Kind.CLIENT: # TODO: trying to set a sa binary annotation for a non-client span # should result in a logged error return remote_endpoint = create_endpoint( port=port, service_name=service_name, host=host, ) if not self.logging_context: if self.remote_endpoint is not None: raise ValueError("SA annotation already set.") self.remote_endpoint = remote_endpoint else: if self.logging_context.remote_endpoint is not None: raise ValueError("SA annotation already set.") self.logging_context.remote_endpoint = remote_endpoint def override_span_name(self, name: str) -> None: """Overrides the current span name. This is useful if you don't know the span name yet when you create the zipkin_span object. i.e. pyramid_zipkin doesn't know which route the request matched until the function wrapped by the context manager completes. :param name: New span name :type name: str """ self.span_name = name if self.logging_context: self.logging_context.span_name = name def _validate_args(kwargs: Dict[str, Any]) -> None: if "kind" in kwargs: raise ValueError( '"kind" is not valid in this context. ' "You probably want to use zipkin_span()" ) class zipkin_client_span(zipkin_span): """Logs a client-side zipkin span. Subclass of :class:`zipkin_span` using only annotations relevant to clients """ def __init__(self, *args: Any, **kwargs: Any) -> None: """Logs a zipkin span with client annotations. See :class:`zipkin_span` for arguments """ _validate_args(kwargs) kwargs["kind"] = Kind.CLIENT super().__init__(*args, **kwargs) class zipkin_server_span(zipkin_span): """Logs a server-side zipkin span. Subclass of :class:`zipkin_span` using only annotations relevant to servers """ def __init__(self, *args: Any, **kwargs: Any) -> None: """Logs a zipkin span with server annotations. See :class:`zipkin_span` for arguments """ _validate_args(kwargs) kwargs["kind"] = Kind.SERVER super().__init__(*args, **kwargs) def create_http_headers_for_new_span( context_stack: Optional[Stack] = None, tracer: Optional[Tracer] = None ) -> Dict[str, Optional[str]]: """ Generate the headers for a new zipkin span. .. note:: If the method is not called from within a zipkin_trace context, empty dict will be returned back. :returns: dict containing (X-B3-TraceId, X-B3-SpanId, X-B3-ParentSpanId, X-B3-Flags and X-B3-Sampled) keys OR an empty dict. """ return create_http_headers(context_stack, tracer, True) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1679663596.5611188 py_zipkin-1.2.8/py_zipkin.egg-info/0000755000175100001730000000000000000000000020046 5ustar00runnerdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663596.0 py_zipkin-1.2.8/py_zipkin.egg-info/PKG-INFO0000644000175100001730000004653100000000000021154 0ustar00runnerdocker00000000000000Metadata-Version: 2.1 Name: py-zipkin Version: 1.2.8 Summary: Library for using Zipkin in Python. Home-page: https://github.com/Yelp/py_zipkin Author: Yelp, Inc. Author-email: opensource+py-zipkin@yelp.com License: Copyright Yelp 2019 Description: [![Build Status](https://travis-ci.org/Yelp/py_zipkin.svg?branch=master)](https://travis-ci.org/Yelp/py_zipkin) [![Coverage Status](https://img.shields.io/coveralls/Yelp/py_zipkin.svg)](https://coveralls.io/r/Yelp/py_zipkin) [![PyPi version](https://img.shields.io/pypi/v/py_zipkin.svg)](https://pypi.python.org/pypi/py_zipkin/) [![Supported Python versions](https://img.shields.io/pypi/pyversions/py_zipkin.svg)](https://pypi.python.org/pypi/py_zipkin/) py_zipkin --------- py_zipkin provides a context manager/decorator along with some utilities to facilitate the usage of Zipkin in Python applications. Install ------- ``` pip install py_zipkin ``` Usage ----- py_zipkin requires a `transport_handler` object that handles logging zipkin messages to a central logging service such as kafka or scribe. `py_zipkin.zipkin.zipkin_span` is the main tool for starting zipkin traces or logging spans inside an ongoing trace. zipkin_span can be used as a context manager or a decorator. #### Usage #1: Start a trace with a given sampling rate ```python from py_zipkin.zipkin import zipkin_span def some_function(a, b): with zipkin_span( service_name='my_service', span_name='my_span_name', transport_handler=some_handler, port=42, sample_rate=0.05, # Value between 0.0 and 100.0 ): do_stuff(a, b) ``` #### Usage #2: Trace a service call The difference between this and Usage #1 is that the zipkin_attrs are calculated separately and passed in, thus negating the need of the sample_rate param. ```python # Define a pyramid tween def tween(request): zipkin_attrs = some_zipkin_attr_creator(request) with zipkin_span( service_name='my_service', span_name='my_span_name', zipkin_attrs=zipkin_attrs, transport_handler=some_handler, port=22, ) as zipkin_context: response = handler(request) zipkin_context.update_binary_annotations( some_binary_annotations) return response ``` #### Usage #3: Log a span inside an ongoing trace This can be also be used inside itself to produce continuously nested spans. ```python @zipkin_span(service_name='my_service', span_name='some_function') def some_function(a, b): return do_stuff(a, b) ``` #### Other utilities `zipkin_span.update_binary_annotations()` can be used inside a zipkin trace to add to the existing set of binary annotations. ```python def some_function(a, b): with zipkin_span( service_name='my_service', span_name='some_function', transport_handler=some_handler, port=42, sample_rate=0.05, ) as zipkin_context: result = do_stuff(a, b) zipkin_context.update_binary_annotations({'result': result}) ``` `zipkin_span.add_sa_binary_annotation()` can be used to add a binary annotation to the current span with the key 'sa'. This function allows the user to specify the destination address of the service being called (useful if the destination doesn't support zipkin). See http://zipkin.io/pages/data_model.html for more information on the 'sa' binary annotation. > NOTE: the V2 span format only support 1 "sa" endpoint (represented by remoteEndpoint) > so `add_sa_binary_annotation` now raises `ValueError` if you try to set multiple "sa" > annotations for the same span. ```python def some_function(): with zipkin_span( service_name='my_service', span_name='some_function', transport_handler=some_handler, port=42, sample_rate=0.05, ) as zipkin_context: make_call_to_non_instrumented_service() zipkin_context.add_sa_binary_annotation( port=123, service_name='non_instrumented_service', host='12.34.56.78', ) ``` `create_http_headers_for_new_span()` creates a set of HTTP headers that can be forwarded in a request to another service. ```python headers = {} headers.update(create_http_headers_for_new_span()) http_client.get( path='some_url', headers=headers, ) ``` Transport --------- py_zipkin (for the moment) thrift-encodes spans. The actual transport layer is pluggable, though. The recommended way to implement a new transport handler is to subclass `py_zipkin.transport.BaseTransportHandler` and implement the `send` and `get_max_payload_bytes` methods. `send` receives an already encoded thrift list as argument. `get_max_payload_bytes` should return the maximum payload size supported by your transport, or `None` if you can send arbitrarily big messages. The simplest way to get spans to the collector is via HTTP POST. Here's an example of a simple HTTP transport using the `requests` library. This assumes your Zipkin collector is running at localhost:9411. > NOTE: older versions of py_zipkin suggested implementing the transport handler > as a function with a single argument. That's still supported and should work > with the current py_zipkin version, but it's deprecated. ```python import requests from py_zipkin.transport import BaseTransportHandler class HttpTransport(BaseTransportHandler): def get_max_payload_bytes(self): return None def send(self, encoded_span): # The collector expects a thrift-encoded list of spans. requests.post( 'http://localhost:9411/api/v1/spans', data=encoded_span, headers={'Content-Type': 'application/x-thrift'}, ) ``` If you have the ability to send spans over Kafka (more like what you might do in production), you'd do something like the following, using the [kafka-python](https://pypi.python.org/pypi/kafka-python) package: ```python from kafka import SimpleProducer, KafkaClient from py_zipkin.transport import BaseTransportHandler class KafkaTransport(BaseTransportHandler): def get_max_payload_bytes(self): # By default Kafka rejects messages bigger than 1000012 bytes. return 1000012 def send(self, message): kafka_client = KafkaClient('{}:{}'.format('localhost', 9092)) producer = SimpleProducer(kafka_client) producer.send_messages('kafka_topic_name', message) ``` Using in multithreading environments ------------------------------------ If you want to use py_zipkin in a cooperative multithreading environment, e.g. asyncio, you need to explicitly pass an instance of `py_zipkin.storage.Stack` as parameter `context_stack` for `zipkin_span` and `create_http_headers_for_new_span`. By default, py_zipkin uses a thread local storage for the attributes, which is defined in `py_zipkin.storage.ThreadLocalStack`. Additionally, you'll also need to explicitly pass an instance of `py_zipkin.storage.SpanStorage` as parameter `span_storage` to `zipkin_span`. ```python from py_zipkin.zipkin import zipkin_span from py_zipkin.storage import Stack from py_zipkin.storage import SpanStorage def my_function(): context_stack = Stack() span_storage = SpanStorage() await my_function(context_stack, span_storage) async def my_function(context_stack, span_storage): with zipkin_span( service_name='my_service', span_name='some_function', transport_handler=some_handler, port=42, sample_rate=0.05, context_stack=context_stack, span_storage=span_storage, ): result = do_stuff(a, b) ``` Firehose mode [EXPERIMENTAL] ---------------------------- "Firehose mode" records 100% of the spans, regardless of sampling rate. This is useful if you want to treat these spans differently, e.g. send them to a different backend that has limited retention. It works in tandem with normal operation, however there may be additional overhead. In order to use this, you add a `firehose_handler` just like you add a `transport_handler`. This feature should be considered experimental and may be removed at any time without warning. If you do use this, be sure to send asynchronously to avoid excess overhead for every request. License ------- Copyright (c) 2018, Yelp, Inc. All Rights reserved. Apache v2 1.2.8 (2023-03-23) ------------------- - Add back exports in py_zipkin.encoding - Fix mypy tests 1.2.7 (2023-02-06) ------------------- - Drop support for Python 3.6 1.2.6 (2023-02-06) ------------------- - Drop support for V1_THRIFT encoding 1.0.0 (2022-06-09) ------------------- - Droop Python 2.7 support (minimal supported python version is 3.5) - Recompile protobuf using version 3.19 0.21.0 (2021-03-17) ------------------- - The default encoding is now V2 JSON. If you want to keep the old V1 thrift encoding you'll need to specify it. 0.20.2 (2021-03-11) ------------------- - Don't crash when annotating exceptions that cannot be str()'d 0.20.1 (2020-10-27) ------------------- - Support PRODUCER and CONSUMER spans 0.20.0 (2020-03-09) ------------------- - Add create_http_headers helper 0.19.0 (2020-02-28) ------------------- - Add zipkin_span.add_annotation() method - Add autoinstrumentation for python Threads - Allow creating a copy of Tracer - Add extract_zipkin_attrs_from_headers() helper 0.18.7 (2020-01-15) ------------------- - Expose encoding.create_endpoint helper 0.18.6 (2019-09-23) ------------------- - Ensure tags are strings when using V2_JSON encoding 0.18.5 (2019-08-08) ------------------- - Add testing.MockTransportHandler module 0.18.4 (2019-08-02) ------------------- - Fix thriftpy2 import to allow cython module 0.18.3 (2019-05-15) ------------------- - Fix unicode bug when decoding thrift tag strings 0.18.2 (2019-03-26) ------------------- - Handled exception while emitting trace and log the error - Ensure tracer is cleared regardless span of emit outcome 0.18.1 (2019-02-22) ------------------- - Fix ThreadLocalStack() bug introduced in 0.18.0 0.18.0 (2019-02-13) ------------------- - Fix multithreading issues - Added Tracer module 0.17.1 (2019-02-05) ------------------- - Ignore transport_handler overrides in an inner span since that causes spans to be dropped. 0.17.0 (2019-01-25) ------------------- - Support python 3.7 - py-zipkin now depends on thriftpy2 rather than thriftpy. They can coexist in the same codebase, so it should be safe to upgrade. 0.16.1 (2018-11-16) ------------------- - Handle null timestamps when decoding thrift traces 0.16.0 (2018-11-13) ------------------- - py_zipkin is now able to convert V1 thrift spans to V2 JSON 0.15.1 (2018-10-31) ------------------- - Changed DeprecationWarnings to logging.warning 0.15.0 (2018-10-22) ------------------- - Added support for V2 JSON encoding. - Fixed TransportHandler bug that was affecting also V1 JSON. 0.14.1 (2018-10-09) ------------------- - Fixed memory leak introduced in 0.13.0. 0.14.0 (2018-10-01) ------------------- - Support JSON encoding for V1 spans. - Allow overriding the span_name after creation. 0.13.0 (2018-06-25) ------------------- - Removed deprecated `zipkin_logger.debug()` interface. - `py_zipkin.stack` was renamed as `py_zipkin.storage`. If you were importing this module, you'll need to update your code. 0.12.0 (2018-05-29) ------------------- - Support max payload size for transport handlers. - Transport handlers should now be implemented as classes extending py_zipkin.transport.BaseTransportHandler. 0.11.2 (2018-05-23) ------------------- - Don't overwrite passed in annotations 0.11.1 (2018-05-23) ------------------- - Add binary annotations to the span even if the request is not being sampled. This fixes binary annotations for firehose spans. 0.11.0 (2018-02-08) ------------------- - Add support for "firehose mode", which logs 100% of the spans regardless of sample rate. 0.10.1 (2018-02-05) ------------------- - context_stack will now default to `ThreadLocalStack()` if passed as `None` 0.10.0 (2018-02-05) ------------------- - Add support for using explicit in-process context storage instead of using thread_local. This allows you to use py_zipkin in cooperative multitasking environments e.g. asyncio - `py_zipkin.thread_local` is now deprecated. Instead use `py_zipkin.stack.ThreadLocalStack()` - TraceId and SpanId generation performance improvements. - 128-bit TraceIds now start with an epoch timestamp to support easy interop with AWS X-Ray 0.9.0 (2017-07-31) ------------------ - Add batch span sending. Note that spans are now sent in lists. 0.8.3 (2017-07-10) ------------------ - Be defensive about having logging handlers configured to avoid throwing NullHandler attribute errors 0.8.2 (2017-06-30) ------------------ - Don't log ss and sr annotations when in a client span context - Add error binary annotation if an exception occurs 0.8.1 (2017-06-16) ------------------ - Fixed server send timing to more accurately reflect when server send actually occurs. - Replaced logging_start annotation with logging_end 0.8.0 (2017-06-01) ------------------ - Added 128-bit trace id support - Added ability to explicitly specify host for a span - Added exception handling if host can't be determined automatically - SERVER_ADDR ('sa') binary annotations can be added to spans - py36 support 0.7.1 (2017-05-01) ------------------ - Fixed a bug where `update_binary_annotations` would fail for a child span in a trace that is not being sampled 0.7.0 (2017-03-06) ------------------ - Simplify `update_binary_annotations` for both root and non-root spans 0.6.0 (2017-02-03) ------------------ - Added support for forcing `zipkin_span` to report timestamp/duration. Changes API of `zipkin_span`, but defaults back to existing behavior. 0.5.0 (2017-02-01) ------------------ - Properly set timestamp/duration on server and local spans - Updated thrift spec to include these new fields - The `zipkin_span` entrypoint should be backwards compatible 0.4.4 (2016-11-29) ------------------ - Add optional annotation for when Zipkin logging starts 0.4.3 (2016-11-04) ------------------ - Fix bug in zipkin_span decorator 0.4.2 (2016-11-01) ------------------ - Be defensive about transport_handler when logging spans. 0.4.1 (2016-10-24) ------------------ - Add ability to override span_id when creating new ZipkinAttrs. 0.4.0 (2016-10-20) ------------------ - Added `start` and `stop` functions as friendlier versions of the __enter__ and __exit__ functions. 0.3.1 (2016-09-30) ------------------ - Adds new param to thrift.create_endpoint allowing creation of thrift Endpoint objects on a proxy machine representing another host. 0.2.1 (2016-09-30) ------------------ - Officially "release" v0.2.0. Accidentally pushed a v0.2.0 without the proper version bump, so v0.2.1 is the new real version. Please use this instead of v0.2.0. 0.2.0 (2016-09-30) ------------------ - Fix problem where if zipkin_attrs and sample_rate were passed, but zipkin_attrs.is_sampled=True, new zipkin_attrs were being generated. 0.1.2 (2016-09-29) ------------------ - Fix sampling algorithm that always sampled for rates > 50% 0.1.1 (2016-07-05) ------------------ - First py_zipkin version with context manager/decorator functionality. Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Provides: py_zipkin Requires-Python: >=3.7 Description-Content-Type: text/markdown Provides-Extra: protobuf ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663596.0 py_zipkin-1.2.8/py_zipkin.egg-info/SOURCES.txt0000644000175100001730000000155700000000000021742 0ustar00runnerdocker00000000000000CHANGELOG.rst LICENSE.txt MANIFEST.in README.md setup.py py_zipkin/__init__.py py_zipkin/exception.py py_zipkin/logging_helper.py py_zipkin/py.typed py_zipkin/request_helpers.py py_zipkin/storage.py py_zipkin/thread_local.py py_zipkin/transport.py py_zipkin/util.py py_zipkin/zipkin.py py_zipkin.egg-info/PKG-INFO py_zipkin.egg-info/SOURCES.txt py_zipkin.egg-info/dependency_links.txt py_zipkin.egg-info/requires.txt py_zipkin.egg-info/top_level.txt py_zipkin/encoding/__init__.py py_zipkin/encoding/_decoders.py py_zipkin/encoding/_encoders.py py_zipkin/encoding/_helpers.py py_zipkin/encoding/_types.py py_zipkin/encoding/protobuf/__init__.py py_zipkin/encoding/protobuf/zipkin_pb2.py py_zipkin/encoding/protobuf/zipkin_pb2.pyi py_zipkin/instrumentations/__init__.py py_zipkin/instrumentations/python_threads.py py_zipkin/testing/__init__.py py_zipkin/testing/mock_transport.py././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663596.0 py_zipkin-1.2.8/py_zipkin.egg-info/dependency_links.txt0000644000175100001730000000000100000000000024114 0ustar00runnerdocker00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663596.0 py_zipkin-1.2.8/py_zipkin.egg-info/requires.txt0000644000175100001730000000007100000000000022444 0ustar00runnerdocker00000000000000typing-extensions>=3.10.0.0 [protobuf] protobuf>=3.12.4 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663596.0 py_zipkin-1.2.8/py_zipkin.egg-info/top_level.txt0000644000175100001730000000001200000000000022571 0ustar00runnerdocker00000000000000py_zipkin ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1679663596.5651188 py_zipkin-1.2.8/setup.cfg0000644000175100001730000000004600000000000016161 0ustar00runnerdocker00000000000000[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1679663591.0 py_zipkin-1.2.8/setup.py0000644000175100001730000000253300000000000016055 0ustar00runnerdocker00000000000000#!/usr/bin/python import os from setuptools import find_packages from setuptools import setup __version__ = '1.2.8' def read(f): return open(os.path.join(os.path.dirname(__file__), f)).read().strip() setup( name='py_zipkin', version=__version__, provides=["py_zipkin"], author='Yelp, Inc.', author_email='opensource+py-zipkin@yelp.com', license='Copyright Yelp 2019', url="https://github.com/Yelp/py_zipkin", description='Library for using Zipkin in Python.', long_description='\n\n'.join((read('README.md'), read('CHANGELOG.rst'))), long_description_content_type="text/markdown", packages=find_packages(exclude=('tests*', 'testing*', 'tools*')), package_data={ 'py_zipkin': ['py.typed'], 'py_zipkin.encoding.protobuf': ['*.pyi'], }, python_requires='>=3.7', install_requires=[ 'typing-extensions>=3.10.0.0', ], extras_require={ 'protobuf': 'protobuf >= 3.12.4', }, classifiers=[ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Topic :: Software Development :: Libraries :: Python Modules", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", ], )